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

130 lines
4.4 KiB
Python

import json
from functools import lru_cache
from typing import Any
from mlflow.entities.trace_data import TraceData
from mlflow.entities.trace_info import TraceInfo
from mlflow.tracing.constant import (
TRACE_REQUEST_RESPONSE_PREVIEW_MAX_LENGTH_DBX,
TRACE_REQUEST_RESPONSE_PREVIEW_MAX_LENGTH_OSS,
)
from mlflow.tracking._tracking_service.utils import get_tracking_uri
from mlflow.utils.uri import is_databricks_uri
def set_request_response_preview(trace_info: TraceInfo, trace_data: TraceData) -> None:
"""
Set the request and response previews for the trace info.
"""
# If request/response preview is already set by users via `mlflow.update_current_trace`,
# we don't override it with the truncated version.
if trace_info.request_preview is None and trace_data.request is not None:
trace_info.request_preview = _get_truncated_preview(trace_data.request, role="user")
if trace_info.response_preview is None and trace_data.response is not None:
trace_info.response_preview = _get_truncated_preview(trace_data.response, role="assistant")
def _get_truncated_preview(
request_or_response: str | dict[str, Any] | None, role: str
) -> str | None:
if request_or_response is None:
return None
max_length = _get_max_length()
content = None
obj = None
if isinstance(request_or_response, dict):
obj = request_or_response
request_or_response = json.dumps(request_or_response)
elif isinstance(request_or_response, str):
try:
obj = json.loads(request_or_response)
except json.JSONDecodeError:
pass
if obj is not None:
if messages := _try_extract_messages(obj):
msg = _get_last_message(messages, role=role)
content = _get_text_content_from_message(msg)
content = content or request_or_response
if len(content) <= max_length:
return content
return content[: max_length - 3] + "..."
@lru_cache(maxsize=1)
def _get_max_length() -> int:
tracking_uri = get_tracking_uri()
return (
TRACE_REQUEST_RESPONSE_PREVIEW_MAX_LENGTH_DBX
if is_databricks_uri(tracking_uri)
else TRACE_REQUEST_RESPONSE_PREVIEW_MAX_LENGTH_OSS
)
def _try_extract_messages(obj: dict[str, Any]) -> list[dict[str, Any]] | None:
if not isinstance(obj, dict):
return None
# Check if the object contains messages with OpenAI ChatCompletion format
if (messages := obj.get("messages")) and isinstance(messages, list):
return [item for item in messages if _is_message(item)]
# Check if the object contains a message in OpenAI ChatCompletion response format (choices)
if (
(choices := obj.get("choices"))
and isinstance(choices, list)
and len(choices) > 0
and isinstance(choices[0], dict)
and (msg := choices[0].get("message"))
and _is_message(msg)
):
return [msg]
# Check if the object contains a message in OpenAI Responses API request format
if (input := obj.get("input")) and isinstance(input, list):
return [item for item in input if _is_message(item)]
# Check if the object contains a message in OpenAI Responses API response format
if (output := obj.get("output")) and isinstance(output, list):
return [item for item in output if _is_message(item)]
# Handle ResponsesAgent input, which contains OpenAI Responses request in 'request' key
if "request" in obj:
return _try_extract_messages(obj["request"])
return None
def _is_message(item: Any) -> bool:
return isinstance(item, dict) and "role" in item and "content" in item
def _get_last_message(messages: list[dict[str, Any]], role: str) -> dict[str, Any]:
"""
Return last message with the given role.
If the messages don't include a message with the given role, return the last one.
"""
for message in reversed(messages):
if message.get("role") == role:
return message
return messages[-1]
def _get_text_content_from_message(message: dict[str, Any]) -> str:
content = message.get("content")
if isinstance(content, list):
# content is a list of content parts
for part in content:
if isinstance(part, str):
return part
elif isinstance(part, dict) and part.get("type") in ["text", "output_text"]:
return part.get("text")
elif isinstance(content, str):
return content
return ""