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

167 lines
6.9 KiB
Python

import logging
from typing import Any
from opentelemetry import context as otel_context_api
from opentelemetry import trace as otel_trace
from opentelemetry.trace import get_current_span
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.kernel_content import KernelContent
from semantic_kernel.contents.streaming_content_mixin import StreamingContentMixin
from semantic_kernel.functions import FunctionResult
from semantic_kernel.utils.telemetry.model_diagnostics import (
gen_ai_attributes as model_gen_ai_attributes,
)
from semantic_kernel.utils.telemetry.model_diagnostics.decorators import (
CHAT_COMPLETION_OPERATION,
TEXT_COMPLETION_OPERATION,
)
from semantic_kernel.utils.telemetry.model_diagnostics.function_tracer import (
OPERATION_NAME as FUNCTION_OPERATION_NAME,
)
import mlflow
from mlflow.entities import SpanType
from mlflow.entities.span import LiveSpan
from mlflow.tracing.constant import SpanAttributeKey, TokenUsageKey
from mlflow.tracing.utils import (
construct_full_inputs,
get_mlflow_span_for_otel_span,
)
_OPERATION_TO_SPAN_TYPE = {
CHAT_COMPLETION_OPERATION: SpanType.CHAT_MODEL,
TEXT_COMPLETION_OPERATION: SpanType.LLM,
FUNCTION_OPERATION_NAME: SpanType.TOOL,
# https://github.com/microsoft/semantic-kernel/blob/d5ee6aa1c176a4b860aba72edaa961570874661b/python/semantic_kernel/utils/telemetry/agent_diagnostics/decorators.py#L22
"invoke_agent": SpanType.AGENT,
}
# NB: Streaming operation names were removed in Semantic Kernel 1.38.0
try:
from semantic_kernel.utils.telemetry.agent_diagnostics.decorators import (
CHAT_STREAMING_COMPLETION_OPERATION,
TEXT_STREAMING_COMPLETION_OPERATION,
)
_OPERATION_TO_SPAN_TYPE[CHAT_STREAMING_COMPLETION_OPERATION] = SpanType.CHAT_MODEL
_OPERATION_TO_SPAN_TYPE[TEXT_STREAMING_COMPLETION_OPERATION] = SpanType.LLM
except ImportError:
pass
_logger = logging.getLogger(__name__)
def semantic_kernel_diagnostics_wrapper(original, *args, **kwargs) -> None:
"""
Wrapper for Semantic Kernel's model diagnostics decorators.
This wrapper is used to record the inputs and outputs to the span, because
Semantic Kernel's Otel span do not record the inputs and outputs.
"""
full_kwargs = construct_full_inputs(original, *args, **kwargs)
current_span = full_kwargs.get("current_span") or get_current_span()
mlflow_span = get_mlflow_span_for_otel_span(current_span)
if not mlflow_span:
_logger.debug("Span is not found or recording. Skipping error handling.")
return original(*args, **kwargs)
if prompt := full_kwargs.get("prompt"):
# Wrapping _set_completion_input
# https://github.com/microsoft/semantic-kernel/blob/d5ee6aa1c176a4b860aba72edaa961570874661b/python/semantic_kernel/utils/telemetry/model_diagnostics/decorators.py#L369
mlflow_span.set_inputs(_parse_content(prompt))
if completions := full_kwargs.get("completions"):
# Wrapping _set_completion_response
# https://github.com/microsoft/semantic-kernel/blob/d5ee6aa1c176a4b860aba72edaa961570874661b/
mlflow_span.set_outputs({"messages": [_parse_content(c) for c in completions]})
if error := full_kwargs.get("error"):
# Wrapping _set_completion_error
# https://github.com/microsoft/semantic-kernel/blob/d5ee6aa1c176a4b860aba72edaa961570874661b/python/semantic_kernel/utils/telemetry/model_diagnostics/decorators.py#L452
mlflow_span.record_exception(error)
return original(*args, **kwargs)
async def patched_kernel_entry_point(original, self, *args, **kwargs):
with mlflow.start_span(
name=f"{self.__class__.__name__}.{original.__name__}",
span_type=SpanType.AGENT,
) as mlflow_span:
inputs = construct_full_inputs(original, self, *args, **kwargs)
mlflow_span.set_inputs(_parse_content(inputs))
# Attach the MLflow span to the global OTel context so that Semantic Kernel's
# internal OTel spans (e.g., execute_tool, chat.completions) will inherit the
# same trace_id and be properly linked as child spans.
global_ctx = otel_trace.set_span_in_context(mlflow_span._span)
token = otel_context_api.attach(global_ctx)
try:
result = await original(self, *args, **kwargs)
finally:
otel_context_api.detach(token)
mlflow_span.set_outputs(_parse_content(result))
return result
def _parse_content(value: Any) -> Any:
"""
Parse the message content objects in Semantic Kernel into a more readable format.
Those objects are Pydantic models, but includes many noisy fields that are not
useful for debugging and hard to read. The base KernelContent class has a to_dict()
method that converts them into more readable format (role, content), so we use that.
"""
if isinstance(value, dict) and (chat_history := value.get("chat_history")):
value = _parse_content(chat_history)
elif isinstance(value, ChatHistory):
# Record chat history as a list of messages for better readability
value = {"messages": [_parse_content(m) for m in value.messages]}
elif isinstance(value, (KernelContent, StreamingContentMixin)):
value = value.to_dict()
elif isinstance(value, FunctionResult):
# Extract "value" field from the FunctionResult object
value = _parse_content(value.value)
elif isinstance(value, list):
value = [_parse_content(item) for item in value]
return value
def set_span_type(mlflow_span: LiveSpan) -> str:
"""Determine the span type based on the operation."""
span_type = SpanType.UNKNOWN
if operation := mlflow_span.get_attribute(model_gen_ai_attributes.OPERATION):
span_type = _OPERATION_TO_SPAN_TYPE.get(operation, SpanType.UNKNOWN)
mlflow_span.set_span_type(span_type)
def set_token_usage(mlflow_span: LiveSpan) -> None:
"""Set token usage attributes on the MLflow span."""
input_tokens = mlflow_span.get_attribute(model_gen_ai_attributes.INPUT_TOKENS)
output_tokens = mlflow_span.get_attribute(model_gen_ai_attributes.OUTPUT_TOKENS)
usage_dict = {}
if input_tokens is not None:
usage_dict[TokenUsageKey.INPUT_TOKENS] = input_tokens
if output_tokens is not None:
usage_dict[TokenUsageKey.OUTPUT_TOKENS] = output_tokens
if input_tokens is not None or output_tokens is not None:
total_tokens = (input_tokens or 0) + (output_tokens or 0)
usage_dict[TokenUsageKey.TOTAL_TOKENS] = total_tokens
if usage_dict:
mlflow_span.set_attribute(SpanAttributeKey.CHAT_USAGE, usage_dict)
def set_model(mlflow_span: LiveSpan) -> None:
"""Set model name and provider attributes on the MLflow span."""
if model := mlflow_span.get_attribute(model_gen_ai_attributes.MODEL):
mlflow_span.set_attribute(SpanAttributeKey.MODEL, model)
if provider := mlflow_span.get_attribute(model_gen_ai_attributes.SYSTEM):
mlflow_span.set_attribute(SpanAttributeKey.MODEL_PROVIDER, provider)