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

367 lines
13 KiB
Python

from unittest import mock
import pytest
from google.protobuf.timestamp_pb2 import Timestamp
from mlflow.entities import (
AssessmentError,
AssessmentSource,
Expectation,
Feedback,
)
from mlflow.entities.trace_info import TraceInfo
from mlflow.entities.trace_location import TraceLocation
from mlflow.entities.trace_state import TraceState
from mlflow.protos.service_pb2 import TraceInfoV3 as ProtoTraceInfoV3
from mlflow.tracing.constant import TRACE_SCHEMA_VERSION, TRACE_SCHEMA_VERSION_KEY
def test_trace_info():
assessments = [
Feedback(
trace_id="trace_id",
name="feedback_test",
value=0.9,
source=AssessmentSource(source_type="HUMAN", source_id="user_1"),
create_time_ms=123456789,
last_update_time_ms=123456789,
error=AssessmentError(error_code="error_code", error_message="Error message"),
rationale="Rationale text",
metadata={"key1": "value1"},
span_id="span_id",
),
Expectation(
trace_id="trace_id",
name="expectation_test",
value=0.8,
source=AssessmentSource(source_type="HUMAN", source_id="user_1"),
create_time_ms=123456789,
last_update_time_ms=123456789,
metadata={"key1": "value1"},
span_id="span_id",
),
]
trace_info = TraceInfo(
trace_id="trace_id",
client_request_id="client_request_id",
trace_location=TraceLocation.from_experiment_id("123"),
request_preview="request",
response_preview="response",
request_time=1234567890,
execution_duration=100,
state=TraceState.OK,
trace_metadata={"foo": "bar", TRACE_SCHEMA_VERSION_KEY: str(TRACE_SCHEMA_VERSION)},
tags={"baz": "qux"},
assessments=assessments,
)
from_proto = TraceInfo.from_proto(trace_info.to_proto())
assert isinstance(from_proto, TraceInfo)
assert from_proto == trace_info
trace_info_dict = trace_info.to_dict()
assert trace_info_dict == {
"trace_id": "trace_id",
"client_request_id": "client_request_id",
"trace_location": {
"type": "MLFLOW_EXPERIMENT",
"mlflow_experiment": {"experiment_id": "123"},
},
"request_preview": "request",
"response_preview": "response",
"request_time": "1970-01-15T06:56:07.890Z",
"execution_duration_ms": 100,
"state": "OK",
"trace_metadata": {"foo": "bar", TRACE_SCHEMA_VERSION_KEY: str(TRACE_SCHEMA_VERSION)},
"assessments": [
{
"assessment_name": "feedback_test",
"trace_id": "trace_id",
"span_id": "span_id",
"source": {"source_type": "HUMAN", "source_id": "user_1"},
"create_time": "1970-01-02T10:17:36.789Z",
"last_update_time": "1970-01-02T10:17:36.789Z",
"feedback": {
"value": 0.9,
"error": {"error_code": "error_code", "error_message": "Error message"},
},
"rationale": "Rationale text",
"metadata": {"key1": "value1"},
"valid": True,
},
{
"assessment_name": "expectation_test",
"trace_id": "trace_id",
"span_id": "span_id",
"source": {"source_type": "HUMAN", "source_id": "user_1"},
"create_time": "1970-01-02T10:17:36.789Z",
"last_update_time": "1970-01-02T10:17:36.789Z",
"expectation": {"value": 0.8},
"metadata": {"key1": "value1"},
},
],
"tags": {"baz": "qux"},
}
assert TraceInfo.from_dict(trace_info_dict) == trace_info
def test_backwards_compatibility_with_v2():
trace_info = TraceInfo(
trace_id="trace_id",
client_request_id="client_request_id",
trace_location=TraceLocation.from_experiment_id("123"),
request_preview="'request'",
response_preview="'response'",
request_time=1234567890,
state=TraceState.OK,
trace_metadata={"foo": "bar"},
tags={"baz": "qux"},
)
assert trace_info.request_id == trace_info.trace_id
assert trace_info.experiment_id == "123"
assert trace_info.request_metadata == {"foo": "bar"}
assert trace_info.timestamp_ms == 1234567890
assert trace_info.execution_time_ms is None
def test_trace_info_v4():
trace_id = "trace:/catalog.schema/test_trace_id"
trace_info_v4 = TraceInfo(
# v4 trace info has URI-format trace id and uc schema location
trace_id=trace_id,
trace_location=TraceLocation.from_databricks_uc_schema(
catalog_name="catalog", schema_name="schema"
),
request_time=0,
state=TraceState.OK,
request_preview="request",
response_preview="response",
client_request_id="client_request_id",
tags={"key": "value"},
)
dict_trace_info_v4 = trace_info_v4.to_dict()
assert dict_trace_info_v4 == {
"trace_id": trace_id,
"trace_location": {
"type": "UC_SCHEMA",
"uc_schema": {
"catalog_name": "catalog",
"schema_name": "schema",
"otel_spans_table_name": "mlflow_experiment_trace_otel_spans",
"otel_logs_table_name": "mlflow_experiment_trace_otel_logs",
},
},
"request_time": mock.ANY,
"state": "OK",
"request_preview": "request",
"response_preview": "response",
"client_request_id": "client_request_id",
"tags": {"key": "value"},
}
assert TraceInfo.from_dict(dict_trace_info_v4) == trace_info_v4
proto_trace_info_v4 = trace_info_v4.to_proto()
assert proto_trace_info_v4.trace_id == "test_trace_id"
assert proto_trace_info_v4.trace_location.uc_schema.catalog_name == "catalog"
assert proto_trace_info_v4.trace_location.uc_schema.schema_name == "schema"
assert proto_trace_info_v4.state == 1
assert proto_trace_info_v4.request_preview == "request"
assert proto_trace_info_v4.response_preview == "response"
assert proto_trace_info_v4.client_request_id == "client_request_id"
assert proto_trace_info_v4.tags == {"key": "value"}
assert len(proto_trace_info_v4.assessments) == 0
assert TraceInfo.from_proto(proto_trace_info_v4) == trace_info_v4
@pytest.mark.parametrize("client_request_id", [None, "client_request_id"])
@pytest.mark.parametrize(
"assessments",
[
[],
# Simple feedback
[
Feedback(
trace_id="trace_id",
name="relevance",
value="The answer is correct",
rationale="Rationale text",
source=AssessmentSource(source_type="LLM_JUDGE", source_id="gpt"),
metadata={"key1": "value1"},
span_id="span_id",
)
],
# Feedback with error
[
Feedback(
trace_id="trace_id",
name="relevance",
error=AssessmentError(error_code="error_code", error_message="Error message"),
)
],
# Simple expectation
[Expectation(trace_id="trace_id", name="relevance", value=0.8)],
# Complex expectation
[
Expectation(
trace_id="trace_id",
name="relevance",
value={"complex": {"expectation": ["structure"]}},
)
],
],
)
def test_trace_info_proto(client_request_id, assessments):
# TraceInfo -> proto
trace_info = TraceInfo(
trace_id="request_id",
client_request_id=client_request_id,
trace_location=TraceLocation.from_experiment_id("test_experiment"),
request_preview="request",
response_preview="response",
request_time=0,
execution_duration=1,
state=TraceState.OK,
trace_metadata={"foo": "bar", TRACE_SCHEMA_VERSION_KEY: str(TRACE_SCHEMA_VERSION)},
tags={"baz": "qux"},
assessments=assessments,
)
proto_trace_info = trace_info.to_proto()
# proto -> TraceInfo
assert TraceInfo.from_proto(proto_trace_info) == trace_info
# TraceInfo -> dict
dict_trace_info = trace_info.to_dict()
assert TraceInfo.from_dict(dict_trace_info) == trace_info
def test_from_proto_excludes_undefined_fields():
"""
Test that undefined fields (client_request_id, execution_duration) are excluded when
constructing a TraceInfo from a protobuf message instance that does not define these fields.
"""
# Manually create a protobuf without setting client_request_id or execution_duration fields
request_time = Timestamp()
request_time.FromMilliseconds(1234567890)
proto = ProtoTraceInfoV3(
trace_id="trace_id",
# Intentionally NOT setting client_request_id
# Intentionally NOT setting execution_duration
trace_location=TraceLocation.from_experiment_id("123").to_proto(),
request_preview="request",
response_preview="response",
request_time=request_time,
state=TraceState.OK.to_proto(),
)
# Verify HasField returns false for undefined fields
assert not proto.HasField("client_request_id")
assert not proto.HasField("execution_duration")
# Convert to TraceInfo
trace_info = TraceInfo.from_proto(proto)
# Verify undefined fields are None
assert trace_info.client_request_id is None
assert trace_info.execution_duration is None
# Verify other fields are correctly populated
assert trace_info.trace_id == "trace_id"
assert trace_info.experiment_id == "123"
assert trace_info.request_preview == "request"
assert trace_info.response_preview == "response"
assert trace_info.request_time == 1234567890
assert trace_info.state == TraceState.OK
def test_trace_info_from_proto_updates_schema_version():
# Create a proto with old schema version in metadata
request_time = Timestamp()
request_time.FromMilliseconds(1234567890)
proto = ProtoTraceInfoV3(
trace_id="test_trace_id",
trace_location=TraceLocation.from_experiment_id("123").to_proto(),
request_preview="test request",
response_preview="test response",
request_time=request_time,
state=TraceState.OK.to_proto(),
trace_metadata={
TRACE_SCHEMA_VERSION_KEY: "2", # Old schema version
"other_key": "other_value",
},
tags={"test_tag": "test_value"},
)
# Convert from proto
trace_info = TraceInfo.from_proto(proto)
# Verify the schema version was updated to current version
assert trace_info.trace_metadata[TRACE_SCHEMA_VERSION_KEY] == "2"
# Verify other metadata is preserved
assert trace_info.trace_metadata["other_key"] == "other_value"
# Verify other fields are correctly populated
assert trace_info.trace_id == "test_trace_id"
assert trace_info.experiment_id == "123"
def test_trace_info_from_proto_preserves_current_schema_version():
# Create a proto with current schema version in metadata
request_time = Timestamp()
request_time.FromMilliseconds(1234567890)
proto = ProtoTraceInfoV3(
trace_id="test_trace_id",
trace_location=TraceLocation.from_experiment_id("123").to_proto(),
request_preview="test request",
response_preview="test response",
request_time=request_time,
state=TraceState.OK.to_proto(),
trace_metadata={
TRACE_SCHEMA_VERSION_KEY: str(TRACE_SCHEMA_VERSION), # Current schema version
"other_key": "other_value",
},
tags={"test_tag": "test_value"},
)
# Convert from proto
trace_info = TraceInfo.from_proto(proto)
# Verify the schema version is preserved as current version
assert trace_info.trace_metadata[TRACE_SCHEMA_VERSION_KEY] == str(TRACE_SCHEMA_VERSION)
# Verify other metadata is preserved
assert trace_info.trace_metadata["other_key"] == "other_value"
def test_trace_info_to_dict_preserves_trace_id():
# Test with v4 URI format
trace_info_v4 = TraceInfo(
trace_id="trace:/catalog.schema/actual_trace_id",
trace_location=TraceLocation.from_databricks_uc_schema(
catalog_name="catalog", schema_name="schema"
),
request_time=0,
state=TraceState.OK,
)
dict_v4 = trace_info_v4.to_dict()
# The dict should have the full trace_id, not the parsed one
assert dict_v4["trace_id"] == "trace:/catalog.schema/actual_trace_id"
# Test with regular trace_id
trace_info = TraceInfo(
trace_id="tr-12345",
trace_location=TraceLocation.from_experiment_id("123"),
request_time=0,
state=TraceState.OK,
)
dict_regular = trace_info.to_dict()
# Regular trace_id should remain unchanged
assert dict_regular["trace_id"] == "tr-12345"