233 lines
6.9 KiB
Python
233 lines
6.9 KiB
Python
import json
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from mlflow.entities.trace_data import TraceData
|
|
from mlflow.entities.trace_info import TraceInfo
|
|
from mlflow.entities.trace_location import TraceLocation
|
|
from mlflow.entities.trace_state import TraceState
|
|
from mlflow.tracing.utils.truncation import _get_truncated_preview, set_request_response_preview
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def patch_max_length():
|
|
# Patch max length to 50 to make tests faster
|
|
with patch("mlflow.tracing.utils.truncation._get_max_length", return_value=50):
|
|
yield
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("input_str", "expected"),
|
|
[
|
|
("short string", "short string"),
|
|
("{'a': 'b'}", "{'a': 'b'}"),
|
|
("start" + "a" * 50, "start" + "a" * 42 + "..."),
|
|
(None, None),
|
|
],
|
|
ids=["short string", "short json", "long string", "none"],
|
|
)
|
|
def test_truncate_simple_string(input_str, expected):
|
|
assert _get_truncated_preview(input_str, role="user") == expected
|
|
|
|
|
|
def test_truncate_long_non_message_json():
|
|
input_str = json.dumps({
|
|
"a": "b" + "a" * 30,
|
|
"b": "c" + "a" * 30,
|
|
})
|
|
result = _get_truncated_preview(input_str, role="user")
|
|
assert len(result) == 50
|
|
assert result.startswith('{"a": "b')
|
|
|
|
|
|
_TEST_MESSAGE_HISTORY = [
|
|
{"role": "user", "content": "First"},
|
|
{"role": "assistant", "content": "Second"},
|
|
{"role": "user", "content": "Third" + "a" * 50},
|
|
{"role": "assistant", "content": "Fourth"},
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"input",
|
|
[
|
|
# ChatCompletion API
|
|
{"messages": _TEST_MESSAGE_HISTORY},
|
|
# Responses API
|
|
{"input": _TEST_MESSAGE_HISTORY},
|
|
# Responses Agent
|
|
{"request": {"input": _TEST_MESSAGE_HISTORY}},
|
|
],
|
|
ids=["chat_completion", "responses", "responses_agent"],
|
|
)
|
|
def test_truncate_request_messages(input):
|
|
input_str = json.dumps(input)
|
|
assert _get_truncated_preview(input_str, role="assistant") == "Fourth"
|
|
# Long content should be truncated
|
|
assert _get_truncated_preview(input_str, role="user") == "Third" + "a" * 42 + "..."
|
|
# If non-existing role is provided, return the last message
|
|
assert _get_truncated_preview(input_str, role="system") == "Fourth"
|
|
|
|
|
|
def test_truncate_request_choices():
|
|
input_str = json.dumps({
|
|
"choices": [
|
|
{
|
|
"index": 1,
|
|
"message": {"role": "assistant", "content": "First" + "a" * 50},
|
|
"finish_reason": "stop",
|
|
},
|
|
],
|
|
"object": "chat.completions",
|
|
})
|
|
assert _get_truncated_preview(input_str, role="assistant").startswith("First")
|
|
|
|
|
|
def test_truncate_multi_content_messages():
|
|
# If text content exists, use it
|
|
assert (
|
|
_get_truncated_preview(
|
|
json.dumps({
|
|
"messages": [{"role": "user", "content": [{"type": "text", "text": "a" * 60}]}]
|
|
}),
|
|
role="user",
|
|
)
|
|
== "a" * 47 + "..."
|
|
)
|
|
|
|
# Ignore non text content
|
|
assert (
|
|
_get_truncated_preview(
|
|
json.dumps({
|
|
"messages": [
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "text", "text": "a" * 60},
|
|
{"type": "image", "image_url": "http://example.com/image.jpg"},
|
|
],
|
|
},
|
|
]
|
|
}),
|
|
role="user",
|
|
)
|
|
== "a" * 47 + "..."
|
|
)
|
|
|
|
# If non-text content exists, truncate the full json as-is
|
|
assert _get_truncated_preview(
|
|
json.dumps({
|
|
"messages": [
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{
|
|
"type": "image",
|
|
"image_url": "http://example.com/image.jpg" + "a" * 50,
|
|
}
|
|
],
|
|
},
|
|
]
|
|
}),
|
|
role="user",
|
|
).startswith('{"messages":')
|
|
|
|
|
|
def test_truncate_responses_api_output():
|
|
input_str = json.dumps({
|
|
"output": [
|
|
{
|
|
"type": "message",
|
|
"id": "test",
|
|
"role": "assistant",
|
|
"content": [{"type": "output_text", "text": "a" * 60}],
|
|
}
|
|
],
|
|
})
|
|
|
|
assert _get_truncated_preview(input_str, role="assistant") == "a" * 47 + "..."
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"input_data",
|
|
[
|
|
{"messages": 123, "long_data": "a" * 50},
|
|
{"messages": []},
|
|
{"input": "string"},
|
|
{"output": 123},
|
|
{"choices": {"0": "value"}},
|
|
{"request": "string"},
|
|
{"choices": [{"message": "not a dict"}]},
|
|
{"choices": [{"message": {"role": "user"}}]},
|
|
],
|
|
)
|
|
def test_truncate_invalid_messages(input_data):
|
|
input_str = json.dumps(input_data)
|
|
result = _get_truncated_preview(input_str, role="user")
|
|
if "long_data" in input_data:
|
|
assert len(result) == 50
|
|
assert result.startswith(input_str[:20])
|
|
else:
|
|
assert result == input_str
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("request_data", "expected_content", "should_not_contain"),
|
|
[
|
|
(
|
|
{"request": {"input": [{"role": "user", "content": "Hello"}]}},
|
|
"Hello",
|
|
"request",
|
|
),
|
|
(
|
|
{"request": {"tool_choice": None, "input": [{"role": "user", "content": "Weather?"}]}},
|
|
"Weather?",
|
|
'"tool_choice"',
|
|
),
|
|
(
|
|
{"request": {"input": [{"role": "user", "content": "Hi"}]}},
|
|
"Hi",
|
|
'"request"',
|
|
),
|
|
],
|
|
ids=["short_structured_json", "agent_format_with_null_fields", "responses_agent_short"],
|
|
)
|
|
def test_truncate_structured_json_extracts_content(
|
|
request_data, expected_content, should_not_contain
|
|
):
|
|
input_str = json.dumps(request_data)
|
|
result = _get_truncated_preview(input_str, role="user")
|
|
assert result == expected_content
|
|
assert should_not_contain not in result
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("content_value", "expected_in_result"),
|
|
[
|
|
(None, '"content": null'),
|
|
("", '"content": ""'),
|
|
(123, '"content": 123'),
|
|
],
|
|
ids=["null_content", "empty_string_content", "numeric_content"],
|
|
)
|
|
def test_truncate_invalid_content_falls_back_to_json(content_value, expected_in_result):
|
|
request_data = {"input": [{"role": "user", "content": content_value}]}
|
|
input_str = json.dumps(request_data)
|
|
result = _get_truncated_preview(input_str, role="user")
|
|
assert expected_in_result in result or result.endswith("...")
|
|
|
|
|
|
def test_set_request_response_preview_skips_none_data():
|
|
trace_info = TraceInfo(
|
|
trace_id="tr-test",
|
|
trace_location=TraceLocation.from_experiment_id("0"),
|
|
request_time=1000,
|
|
state=TraceState.OK,
|
|
)
|
|
trace_data = TraceData(spans=[], request=None, response=None)
|
|
set_request_response_preview(trace_info, trace_data)
|
|
|
|
assert trace_info.request_preview is None
|
|
assert trace_info.response_preview is None
|