chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
from typing import Any
|
||||
|
||||
|
||||
async def run_in_executor(executor: Any, func: Callable, *args, **kwargs) -> Any:
|
||||
"""Run a function in an executor."""
|
||||
return await asyncio.get_event_loop().run_in_executor(executor, partial(func, *args, **kwargs))
|
||||
@@ -0,0 +1,5 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from semantic_kernel.utils.authentication.entra_id_authentication import get_entra_auth_token
|
||||
|
||||
__all__ = ["get_entra_auth_token"]
|
||||
@@ -0,0 +1,37 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import logging
|
||||
|
||||
from azure.core.credentials import TokenCredential
|
||||
from azure.core.exceptions import ClientAuthenticationError
|
||||
|
||||
from semantic_kernel.exceptions.service_exceptions import ServiceInvalidAuthError
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_entra_auth_token(credential: "TokenCredential", token_endpoint: str) -> str | None:
|
||||
"""Retrieve a Microsoft Entra Auth Token for a given token endpoint.
|
||||
|
||||
The token endpoint may be specified as an environment variable, via the .env
|
||||
file or as an argument. If the token endpoint is not provided, the default is None.
|
||||
|
||||
Args:
|
||||
credential: The credential to use to retrieve the authentication token.
|
||||
token_endpoint: The token endpoint to use to retrieve the authentication token.
|
||||
|
||||
Returns:
|
||||
The Azure token or None if the token could not be retrieved.
|
||||
"""
|
||||
if not token_endpoint:
|
||||
raise ServiceInvalidAuthError(
|
||||
"A token endpoint must be provided either in settings, as an environment variable, or as an argument."
|
||||
)
|
||||
|
||||
try:
|
||||
auth_token = credential.get_token(token_endpoint)
|
||||
except ClientAuthenticationError:
|
||||
logger.error(f"Failed to retrieve Azure token for the specified endpoint: `{token_endpoint}`.")
|
||||
return None
|
||||
|
||||
return auth_token.token if auth_token else None
|
||||
@@ -0,0 +1,23 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from semantic_kernel.contents.chat_history import ChatHistory
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.contents.chat_message_content import ChatMessageContent
|
||||
|
||||
|
||||
def store_results(chat_history: ChatHistory, results: list["ChatMessageContent"]) -> ChatHistory:
|
||||
"""Stores specific results in the context and chat prompt.
|
||||
|
||||
Args:
|
||||
chat_history(ChatHistory): The current chat history instance.
|
||||
results(list["ChatMessageContent"]): Messages to be stored in the history.
|
||||
|
||||
Returns:
|
||||
ChatHistory: Updated chat history containing the new messages.
|
||||
"""
|
||||
for message in results:
|
||||
chat_history.add_message(message=message)
|
||||
return chat_history
|
||||
@@ -0,0 +1,153 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any, TypeVar, cast
|
||||
|
||||
T = TypeVar("T", bound=type[Any] | Callable[..., Any])
|
||||
|
||||
DEFAULT_RC_NOTE = (
|
||||
"Features marked with this status are nearing completion and are considered "
|
||||
"stable for most purposes, but may still incur minor refinements or "
|
||||
"optimizations before achieving full general availability."
|
||||
)
|
||||
|
||||
"""
|
||||
Example usage:
|
||||
|
||||
@experimental
|
||||
class MyExperimentalClass:
|
||||
'''A class that is still evolving rapidly.'''
|
||||
pass
|
||||
|
||||
@stage(status="experimental")
|
||||
class MyExperimentalClass:
|
||||
'''A class that is still evolving rapidly.'''
|
||||
pass
|
||||
|
||||
@experimental
|
||||
def my_experimental_function():
|
||||
'''A function that is still evolving rapidly.'''
|
||||
pass
|
||||
|
||||
@release_candidate
|
||||
class MyRCClass:
|
||||
'''A class that is nearly final, but still in release-candidate stage.'''
|
||||
pass
|
||||
|
||||
@release_candidate("1.23.1-rc1")
|
||||
class MyRCClass:
|
||||
'''A class that is nearly final, but still in release-candidate stage.'''
|
||||
pass
|
||||
"""
|
||||
|
||||
|
||||
def _update_docstring(obj: T, note: str) -> None:
|
||||
"""Append or set the docstring of the given object with the specified note."""
|
||||
if obj.__doc__:
|
||||
obj.__doc__ += f"\n\n{note}"
|
||||
else:
|
||||
obj.__doc__ = note
|
||||
|
||||
|
||||
def stage(
|
||||
status: str = "experimental",
|
||||
version: str | None = None,
|
||||
note: str | None = None,
|
||||
) -> Callable[[T], T]:
|
||||
"""A general-purpose decorator for marking a function or a class.
|
||||
|
||||
It updates the docstring and attaches 'stage_status' (and optionally
|
||||
'stage_version') as metadata. A custom 'note' may be provided to
|
||||
override the default appended text.
|
||||
|
||||
Args:
|
||||
status: The development stage (e.g., 'experimental', 'release_candidate', etc.).
|
||||
version: Optional version or release info (e.g., '1.21.0-rc4').
|
||||
note: A custom note to append to the docstring. If omitted, a default
|
||||
note is used to indicate the stage and possible changes.
|
||||
|
||||
Returns:
|
||||
A decorator that updates the docstring and metadata of
|
||||
the target function/class.
|
||||
"""
|
||||
|
||||
def decorator(obj: T) -> T:
|
||||
entity_type = "class" if isinstance(obj, type) else "function"
|
||||
ver_text = f" (Version: {version})" if version else ""
|
||||
default_note = f"Note: This {entity_type} is marked as '{status}'{ver_text} and may change in the future."
|
||||
final_note = note if note else default_note
|
||||
|
||||
_update_docstring(obj, final_note)
|
||||
setattr(obj, "stage_status", status)
|
||||
if version:
|
||||
setattr(obj, "stage_version", version)
|
||||
|
||||
return obj
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def experimental(obj: T) -> T:
|
||||
"""Decorator specifically for 'experimental' features.
|
||||
|
||||
It uses the general 'stage' decorator but also attaches
|
||||
'is_experimental = True'.
|
||||
"""
|
||||
decorated = stage(status="experimental")(obj)
|
||||
setattr(decorated, "is_experimental", True)
|
||||
return decorated
|
||||
|
||||
|
||||
def release_candidate(
|
||||
func: T | str | None = None,
|
||||
*,
|
||||
version: str | None = None,
|
||||
doc_string: str | None = None,
|
||||
) -> T:
|
||||
"""Decorator that designates a function/class as being in a 'release candidate' state.
|
||||
|
||||
By default, applies a descriptive note indicating near-completion and possible minor refinements
|
||||
before achieving general availability. You may override this with a custom 'doc_string' if needed.
|
||||
|
||||
Usage:
|
||||
1) @release_candidate
|
||||
2) @release_candidate()
|
||||
3) @release_candidate("1.21.3-rc1")
|
||||
4) @release_candidate(version="1.21.3-rc1")
|
||||
5) @release_candidate(doc_string="Custom RC note...")
|
||||
6) @release_candidate(version="1.21.3-rc1", doc_string="Custom RC note...")
|
||||
|
||||
Args:
|
||||
func:
|
||||
- In cases (1) or (2), this is the function/class being decorated.
|
||||
- In cases (3) or (4), this may be a version string or None.
|
||||
version:
|
||||
The RC version string, if provided.
|
||||
doc_string:
|
||||
An optional custom note to append to the docstring, overriding
|
||||
the default RC descriptive note.
|
||||
|
||||
Returns:
|
||||
The decorated object, with an updated docstring and
|
||||
'is_release_candidate = True'.
|
||||
"""
|
||||
from semantic_kernel import DEFAULT_RC_VERSION
|
||||
|
||||
def _apply(obj: T, ver: str, note: str | None) -> T:
|
||||
ver_text = f" (Version: {ver})" if ver else ""
|
||||
rc_note = note if note is not None else f"{DEFAULT_RC_NOTE}{ver_text}"
|
||||
|
||||
decorated = stage(status="release_candidate", version=ver, note=rc_note)(obj)
|
||||
setattr(decorated, "is_release_candidate", True)
|
||||
return decorated
|
||||
|
||||
if func is not None and callable(func):
|
||||
ver = version or DEFAULT_RC_VERSION
|
||||
return _apply(cast(T, func), ver, doc_string)
|
||||
|
||||
ver_str = func if isinstance(func, str) else version
|
||||
|
||||
def wrapper(obj: T) -> T:
|
||||
return _apply(obj, ver_str or DEFAULT_RC_VERSION, doc_string)
|
||||
|
||||
return wrapper # type: ignore
|
||||
@@ -0,0 +1,21 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator, AsyncIterable, Sequence
|
||||
from typing import TypeVar
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
async def desync_list(sync_list: Sequence[_T]) -> AsyncIterable[_T]: # noqa: RUF029
|
||||
"""De synchronize a list of synchronous objects."""
|
||||
for x in sync_list:
|
||||
yield x
|
||||
|
||||
|
||||
async def empty_generator() -> AsyncGenerator[_T, None]:
|
||||
"""An empty generator, can be used to return an empty generator."""
|
||||
if False:
|
||||
yield None
|
||||
await asyncio.sleep(0)
|
||||
@@ -0,0 +1,11 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
def setup_logging():
|
||||
"""Setup a detailed logging format."""
|
||||
logging.basicConfig(
|
||||
format="[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import random
|
||||
import string
|
||||
|
||||
|
||||
def generate_random_ascii_name(length: int = 16) -> str:
|
||||
"""Generate a series of random ASCII characters of the specified length.
|
||||
|
||||
As example, plugin/function names can contain upper/lowercase letters, and underscores
|
||||
|
||||
Args:
|
||||
length (int): The length of the string to generate.
|
||||
|
||||
Returns:
|
||||
A string of random ASCII characters of the specified length.
|
||||
"""
|
||||
letters = string.ascii_letters
|
||||
return "".join(random.choices(letters, k=length)) # nosec
|
||||
@@ -0,0 +1,253 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import functools
|
||||
import json
|
||||
from collections.abc import AsyncIterable, Awaitable, Callable
|
||||
from functools import reduce
|
||||
from typing import ParamSpec, cast
|
||||
|
||||
from opentelemetry.trace import Span, StatusCode, get_tracer
|
||||
|
||||
from semantic_kernel.agents.agent import Agent, AgentResponseItem
|
||||
from semantic_kernel.contents.chat_message_content import ChatMessageContent
|
||||
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
|
||||
from semantic_kernel.contents.utils.author_role import AuthorRole
|
||||
from semantic_kernel.utils.feature_stage_decorator import experimental
|
||||
from semantic_kernel.utils.telemetry.agent_diagnostics import gen_ai_attributes
|
||||
from semantic_kernel.utils.telemetry.model_diagnostics.model_diagnostics_settings import ModelDiagnosticSettings
|
||||
|
||||
# Module to instrument GenAI agents using OpenTelemetry and OpenTelemetry Semantic Conventions.
|
||||
# These are experimental features and may change in the future.
|
||||
|
||||
# To enable these features, set one of the following environment variables to true:
|
||||
# SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS
|
||||
# SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE
|
||||
# We are re-using the model diagnostic settings to control the instrumentation of agents
|
||||
# because it makes sense to have a system wide setting for diagnostics. The name "model"
|
||||
# is a legacy name because the concept of agent was not yet introduced when the settings were created.
|
||||
MODEL_DIAGNOSTICS_SETTINGS = ModelDiagnosticSettings()
|
||||
|
||||
P = ParamSpec("P")
|
||||
|
||||
# Creates a tracer from the global tracer provider
|
||||
tracer = get_tracer(__name__)
|
||||
|
||||
OPERATION_NAME = "invoke_agent"
|
||||
|
||||
|
||||
@experimental
|
||||
def are_model_diagnostics_enabled() -> bool:
|
||||
"""Check if model diagnostics are enabled.
|
||||
|
||||
Model diagnostics are enabled if either diagnostic is enabled or diagnostic with sensitive events is enabled.
|
||||
"""
|
||||
return (
|
||||
MODEL_DIAGNOSTICS_SETTINGS.enable_otel_diagnostics
|
||||
or MODEL_DIAGNOSTICS_SETTINGS.enable_otel_diagnostics_sensitive
|
||||
)
|
||||
|
||||
|
||||
@experimental
|
||||
def are_sensitive_events_enabled() -> bool:
|
||||
"""Check if sensitive events are enabled.
|
||||
|
||||
Sensitive events are enabled if the diagnostic with sensitive events is enabled.
|
||||
"""
|
||||
return MODEL_DIAGNOSTICS_SETTINGS.enable_otel_diagnostics_sensitive
|
||||
|
||||
|
||||
@experimental
|
||||
def trace_agent_get_response(
|
||||
get_response_func: Callable[P, Awaitable[AgentResponseItem[ChatMessageContent]]],
|
||||
) -> Callable[P, Awaitable[AgentResponseItem[ChatMessageContent]]]:
|
||||
"""Decorator to trace agent invocation."""
|
||||
|
||||
@functools.wraps(get_response_func)
|
||||
async def wrapper_decorator(
|
||||
*args: P.args,
|
||||
**kwargs: P.kwargs,
|
||||
) -> AgentResponseItem[ChatMessageContent]:
|
||||
if not are_model_diagnostics_enabled():
|
||||
# If model diagnostics are not enabled, just return the responses
|
||||
return await get_response_func(*args, **kwargs)
|
||||
|
||||
agent = cast(Agent, args[0])
|
||||
messages = args[1] if len(args) > 1 else None
|
||||
|
||||
with _start_as_current_span(agent) as span:
|
||||
try:
|
||||
_set_agent_invocation_input(span, messages) # type: ignore
|
||||
response = await get_response_func(*args, **kwargs)
|
||||
_set_agent_invocation_output(span, [response.message])
|
||||
return response
|
||||
except Exception as e:
|
||||
_set_agent_invocation_error(span, e)
|
||||
raise
|
||||
|
||||
# Mark the wrapper decorator as an agent diagnostics decorator
|
||||
wrapper_decorator.__agent_diagnostics__ = True # type: ignore
|
||||
|
||||
return wrapper_decorator
|
||||
|
||||
|
||||
@experimental
|
||||
def trace_agent_invocation(
|
||||
invoke_func: Callable[P, AsyncIterable[AgentResponseItem[ChatMessageContent]]],
|
||||
) -> Callable[P, AsyncIterable[AgentResponseItem[ChatMessageContent]]]:
|
||||
"""Decorator to trace agent invocation."""
|
||||
|
||||
@functools.wraps(invoke_func)
|
||||
async def wrapper_decorator(
|
||||
*args: P.args,
|
||||
**kwargs: P.kwargs,
|
||||
) -> AsyncIterable[AgentResponseItem[ChatMessageContent]]:
|
||||
if not are_model_diagnostics_enabled():
|
||||
# If model diagnostics are not enabled, just return the responses
|
||||
async for response in invoke_func(*args, **kwargs):
|
||||
yield response
|
||||
return
|
||||
|
||||
agent = cast(Agent, args[0])
|
||||
messages = args[1] if len(args) > 1 else None
|
||||
|
||||
with _start_as_current_span(agent) as current_span:
|
||||
_set_agent_invocation_input(current_span, messages) # type: ignore
|
||||
try:
|
||||
responses: list[ChatMessageContent] = []
|
||||
async for response in invoke_func(*args, **kwargs):
|
||||
responses.append(response.message)
|
||||
yield response
|
||||
_set_agent_invocation_output(current_span, responses)
|
||||
except Exception as e:
|
||||
_set_agent_invocation_error(current_span, e)
|
||||
raise
|
||||
|
||||
# Mark the wrapper decorator as an agent diagnostics decorator
|
||||
wrapper_decorator.__agent_diagnostics__ = True # type: ignore
|
||||
|
||||
return wrapper_decorator
|
||||
|
||||
|
||||
@experimental
|
||||
def trace_agent_streaming_invocation(
|
||||
invoke_func: Callable[P, AsyncIterable[AgentResponseItem[StreamingChatMessageContent]]],
|
||||
) -> Callable[P, AsyncIterable[AgentResponseItem[StreamingChatMessageContent]]]:
|
||||
"""Decorator to trace agent streaming invocation."""
|
||||
|
||||
@functools.wraps(invoke_func)
|
||||
async def wrapper_decorator(
|
||||
*args: P.args,
|
||||
**kwargs: P.kwargs,
|
||||
) -> AsyncIterable[AgentResponseItem[StreamingChatMessageContent]]:
|
||||
if not are_model_diagnostics_enabled():
|
||||
# If model diagnostics are not enabled, just return the responses
|
||||
async for chunk in invoke_func(*args, **kwargs):
|
||||
yield chunk
|
||||
return
|
||||
|
||||
agent = cast(Agent, args[0])
|
||||
messages = args[1] if len(args) > 1 else None
|
||||
|
||||
with _start_as_current_span(agent) as current_span:
|
||||
_set_agent_invocation_input(current_span, messages) # type: ignore
|
||||
try:
|
||||
chunks: list[StreamingChatMessageContent] = []
|
||||
async for chunk in invoke_func(*args, **kwargs):
|
||||
chunks.append(chunk.message)
|
||||
yield chunk
|
||||
# Concatenate the streaming chunks
|
||||
if chunks:
|
||||
response = reduce(lambda x, y: x + y, chunks)
|
||||
_set_agent_invocation_output(current_span, [response])
|
||||
else:
|
||||
_set_agent_invocation_output(current_span, [])
|
||||
except Exception as e:
|
||||
_set_agent_invocation_error(current_span, e)
|
||||
raise
|
||||
|
||||
# Mark the wrapper decorator as an agent diagnostics decorator
|
||||
wrapper_decorator.__agent_diagnostics__ = True # type: ignore
|
||||
|
||||
return wrapper_decorator
|
||||
|
||||
|
||||
def _start_as_current_span(agent: Agent):
|
||||
"""Starts a span for the given agent.
|
||||
|
||||
Args:
|
||||
agent (Agent): The agent for which to start the span.
|
||||
|
||||
Returns:
|
||||
Span: The started span as a context manager.
|
||||
"""
|
||||
attributes = {
|
||||
gen_ai_attributes.OPERATION: OPERATION_NAME,
|
||||
gen_ai_attributes.AGENT_ID: agent.id,
|
||||
gen_ai_attributes.AGENT_NAME: agent.name,
|
||||
}
|
||||
|
||||
if agent.description:
|
||||
attributes[gen_ai_attributes.AGENT_DESCRIPTION] = agent.description
|
||||
|
||||
if agent.kernel.plugins:
|
||||
# This will only capture the tools that are available in the kernel at the time of agent creation.
|
||||
# If the agent is invoked with another kernel instance, the tools in that kernel will not be captured.
|
||||
from semantic_kernel.connectors.ai.function_calling_utils import (
|
||||
kernel_function_metadata_to_function_call_format,
|
||||
)
|
||||
|
||||
tool_definitions = [
|
||||
kernel_function_metadata_to_function_call_format(metadata)
|
||||
for metadata in agent.kernel.get_full_list_of_function_metadata()
|
||||
]
|
||||
attributes[gen_ai_attributes.AGENT_TOOL_DEFINITIONS] = json.dumps(tool_definitions)
|
||||
|
||||
return tracer.start_as_current_span(f"{OPERATION_NAME} {agent.name}", attributes=attributes)
|
||||
|
||||
|
||||
def _set_agent_invocation_input(
|
||||
current_span: Span,
|
||||
messages: str | ChatMessageContent | list[str | ChatMessageContent] | None,
|
||||
) -> None:
|
||||
"""Set the agent input attributes in the span."""
|
||||
if are_sensitive_events_enabled():
|
||||
parsed_messages = _parse_agent_invocation_messages(messages)
|
||||
current_span.set_attribute(
|
||||
gen_ai_attributes.AGENT_INVOCATION_INPUT,
|
||||
json.dumps([message.to_dict() for message in parsed_messages]),
|
||||
)
|
||||
|
||||
|
||||
def _set_agent_invocation_output(current_span: Span, response: list[ChatMessageContent]) -> None:
|
||||
"""Set the agent output attributes in the span."""
|
||||
if are_sensitive_events_enabled():
|
||||
current_span.set_attribute(
|
||||
gen_ai_attributes.AGENT_INVOCATION_OUTPUT,
|
||||
json.dumps([message.to_dict() for message in response]),
|
||||
)
|
||||
|
||||
|
||||
def _set_agent_invocation_error(current_span: Span, error: Exception) -> None:
|
||||
"""Set the agent error attributes in the span."""
|
||||
current_span.set_attribute(gen_ai_attributes.ERROR_TYPE, type(error).__name__)
|
||||
current_span.set_status(StatusCode.ERROR, repr(error))
|
||||
|
||||
|
||||
def _parse_agent_invocation_messages(
|
||||
messages: str | ChatMessageContent | list[str | ChatMessageContent] | None,
|
||||
) -> list[ChatMessageContent]:
|
||||
"""Parse the agent invocation messages into a list of ChatMessageContent."""
|
||||
if not messages:
|
||||
return []
|
||||
|
||||
if isinstance(messages, str):
|
||||
return [ChatMessageContent(role=AuthorRole.USER, content=messages)]
|
||||
if isinstance(messages, ChatMessageContent):
|
||||
return [messages]
|
||||
if isinstance(messages, list):
|
||||
return [
|
||||
msg if isinstance(msg, ChatMessageContent) else ChatMessageContent(role=AuthorRole.USER, content=msg)
|
||||
for msg in messages
|
||||
]
|
||||
|
||||
return []
|
||||
@@ -0,0 +1,16 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
# Constants for tracing agent activities with semantic conventions.
|
||||
# Ideally, we should use the attributes from the semcov package.
|
||||
# However, many of the attributes are not yet available in the package,
|
||||
# so we define them here for now.
|
||||
|
||||
# Activity tags
|
||||
OPERATION = "gen_ai.operation.name"
|
||||
AGENT_ID = "gen_ai.agent.id"
|
||||
AGENT_NAME = "gen_ai.agent.name"
|
||||
AGENT_DESCRIPTION = "gen_ai.agent.description"
|
||||
AGENT_INVOCATION_INPUT = "gen_ai.input.messages"
|
||||
AGENT_INVOCATION_OUTPUT = "gen_ai.output.messages"
|
||||
AGENT_TOOL_DEFINITIONS = "gen_ai.tool.definitions"
|
||||
ERROR_TYPE = "error.type"
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from semantic_kernel.utils.telemetry.model_diagnostics.decorators import (
|
||||
trace_chat_completion,
|
||||
trace_streaming_chat_completion,
|
||||
trace_streaming_text_completion,
|
||||
trace_text_completion,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"trace_chat_completion",
|
||||
"trace_streaming_chat_completion",
|
||||
"trace_streaming_text_completion",
|
||||
"trace_text_completion",
|
||||
]
|
||||
@@ -0,0 +1,453 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import functools
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import AsyncGenerator, Callable
|
||||
from functools import reduce
|
||||
from typing import TYPE_CHECKING, Any, ClassVar
|
||||
|
||||
from opentelemetry.trace import Span, StatusCode, get_tracer, use_span
|
||||
|
||||
from semantic_kernel.connectors.ai.completion_usage import CompletionUsage
|
||||
from semantic_kernel.contents.chat_history import ChatHistory
|
||||
from semantic_kernel.contents.chat_message_content import ChatMessageContent
|
||||
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
|
||||
from semantic_kernel.contents.streaming_content_mixin import StreamingContentMixin
|
||||
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
|
||||
from semantic_kernel.contents.text_content import TextContent
|
||||
from semantic_kernel.utils.feature_stage_decorator import experimental
|
||||
from semantic_kernel.utils.telemetry.model_diagnostics import gen_ai_attributes
|
||||
from semantic_kernel.utils.telemetry.model_diagnostics.model_diagnostics_settings import ModelDiagnosticSettings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
|
||||
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
|
||||
from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase
|
||||
|
||||
# Module to instrument GenAI models using OpenTelemetry and OpenTelemetry Semantic Conventions.
|
||||
# These are experimental features and may change in the future.
|
||||
|
||||
# To enable these features, set one of the following environment variables to true:
|
||||
# SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS
|
||||
# SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE
|
||||
MODEL_DIAGNOSTICS_SETTINGS = ModelDiagnosticSettings()
|
||||
|
||||
# Operation names
|
||||
CHAT_COMPLETION_OPERATION = "chat"
|
||||
TEXT_COMPLETION_OPERATION = "text_completions"
|
||||
|
||||
|
||||
# We're recording multiple events for the chat history, some of them are emitted within (hundreds of)
|
||||
# nanoseconds of each other. The default timestamp resolution is not high enough to guarantee unique
|
||||
# timestamps for each message. Also Azure Monitor truncates resolution to microseconds and some other
|
||||
# backends truncate to milliseconds.
|
||||
#
|
||||
# But we need to give users a way to restore chat message order, so we're incrementing the timestamp
|
||||
# by 1 microsecond for each message.
|
||||
#
|
||||
# This is a workaround, we'll find a generic and better solution - see
|
||||
# https://github.com/open-telemetry/semantic-conventions/issues/1701
|
||||
class ChatHistoryMessageTimestampFilter(logging.Filter):
|
||||
"""A filter to increment the timestamp of INFO logs by 1 microsecond."""
|
||||
|
||||
INDEX_KEY: ClassVar[str] = "CHAT_MESSAGE_INDEX"
|
||||
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
"""Increment the timestamp of INFO logs by 1 microsecond."""
|
||||
if hasattr(record, self.INDEX_KEY):
|
||||
idx = getattr(record, self.INDEX_KEY)
|
||||
record.created += idx * 1e-6
|
||||
return True
|
||||
|
||||
|
||||
# Creates a tracer from the global tracer provider
|
||||
tracer = get_tracer(__name__)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.addFilter(ChatHistoryMessageTimestampFilter())
|
||||
|
||||
|
||||
@experimental
|
||||
def are_model_diagnostics_enabled() -> bool:
|
||||
"""Check if model diagnostics are enabled.
|
||||
|
||||
Model diagnostics are enabled if either diagnostic is enabled or diagnostic with sensitive events is enabled.
|
||||
"""
|
||||
return (
|
||||
MODEL_DIAGNOSTICS_SETTINGS.enable_otel_diagnostics
|
||||
or MODEL_DIAGNOSTICS_SETTINGS.enable_otel_diagnostics_sensitive
|
||||
)
|
||||
|
||||
|
||||
@experimental
|
||||
def are_sensitive_events_enabled() -> bool:
|
||||
"""Check if sensitive events are enabled.
|
||||
|
||||
Sensitive events are enabled if the diagnostic with sensitive events is enabled.
|
||||
"""
|
||||
return MODEL_DIAGNOSTICS_SETTINGS.enable_otel_diagnostics_sensitive
|
||||
|
||||
|
||||
@experimental
|
||||
def trace_chat_completion(model_provider: str) -> Callable:
|
||||
"""Decorator to trace chat completion activities.
|
||||
|
||||
Args:
|
||||
model_provider (str): The model provider should describe a family of
|
||||
GenAI models with specific model identified by ai_model_id. For example,
|
||||
model_provider could be "openai" and ai_model_id could be "gpt-3.5-turbo".
|
||||
Sometimes the model provider is unknown at runtime, in which case it can be
|
||||
set to the most specific known provider. For example, while using local models
|
||||
hosted by Ollama, the model provider could be set to "ollama".
|
||||
"""
|
||||
|
||||
def inner_trace_chat_completion(completion_func: Callable) -> Callable:
|
||||
@functools.wraps(completion_func)
|
||||
async def wrapper_decorator(*args: Any, **kwargs: Any) -> list[ChatMessageContent]:
|
||||
if not are_model_diagnostics_enabled():
|
||||
# If model diagnostics are not enabled, just return the completion
|
||||
return await completion_func(*args, **kwargs)
|
||||
|
||||
completion_service: "ChatCompletionClientBase" = args[0]
|
||||
chat_history: ChatHistory = kwargs.get("chat_history") or args[1] # type: ignore
|
||||
settings: "PromptExecutionSettings" = kwargs.get("settings") or args[2] # type: ignore
|
||||
|
||||
with use_span(
|
||||
_get_completion_span(
|
||||
CHAT_COMPLETION_OPERATION,
|
||||
completion_service.ai_model_id,
|
||||
model_provider,
|
||||
completion_service.service_url(),
|
||||
settings,
|
||||
),
|
||||
end_on_exit=True,
|
||||
) as current_span:
|
||||
_set_completion_input(model_provider, chat_history)
|
||||
try:
|
||||
completions: list[ChatMessageContent] = await completion_func(*args, **kwargs)
|
||||
_set_completion_response(current_span, completions, model_provider)
|
||||
return completions
|
||||
except Exception as exception:
|
||||
_set_completion_error(current_span, exception)
|
||||
raise
|
||||
|
||||
# Mark the wrapper decorator as a chat completion decorator
|
||||
wrapper_decorator.__model_diagnostics_chat_completion__ = True # type: ignore
|
||||
|
||||
return wrapper_decorator
|
||||
|
||||
return inner_trace_chat_completion
|
||||
|
||||
|
||||
@experimental
|
||||
def trace_streaming_chat_completion(model_provider: str) -> Callable:
|
||||
"""Decorator to trace streaming chat completion activities.
|
||||
|
||||
Args:
|
||||
model_provider (str): The model provider should describe a family of
|
||||
GenAI models with specific model identified by ai_model_id. For example,
|
||||
model_provider could be "openai" and ai_model_id could be "gpt-3.5-turbo".
|
||||
Sometimes the model provider is unknown at runtime, in which case it can be
|
||||
set to the most specific known provider. For example, while using local models
|
||||
hosted by Ollama, the model provider could be set to "ollama".
|
||||
"""
|
||||
|
||||
def inner_trace_streaming_chat_completion(completion_func: Callable) -> Callable:
|
||||
@functools.wraps(completion_func)
|
||||
async def wrapper_decorator(
|
||||
*args: Any, **kwargs: Any
|
||||
) -> AsyncGenerator[list["StreamingChatMessageContent"], Any]:
|
||||
if not are_model_diagnostics_enabled():
|
||||
# If model diagnostics are not enabled, just return the completion
|
||||
async for streaming_chat_message_contents in completion_func(*args, **kwargs):
|
||||
yield streaming_chat_message_contents
|
||||
return
|
||||
|
||||
completion_service: "ChatCompletionClientBase" = args[0]
|
||||
chat_history: ChatHistory = kwargs.get("chat_history") or args[1] # type: ignore
|
||||
settings: "PromptExecutionSettings" = kwargs.get("settings") or args[2] # type: ignore
|
||||
|
||||
all_messages: dict[int, list[StreamingChatMessageContent]] = {}
|
||||
|
||||
with use_span(
|
||||
_get_completion_span(
|
||||
CHAT_COMPLETION_OPERATION,
|
||||
completion_service.ai_model_id,
|
||||
model_provider,
|
||||
completion_service.service_url(),
|
||||
settings,
|
||||
),
|
||||
end_on_exit=True,
|
||||
) as current_span:
|
||||
_set_completion_input(model_provider, chat_history)
|
||||
try:
|
||||
async for streaming_chat_message_contents in completion_func(*args, **kwargs):
|
||||
for streaming_chat_message_content in streaming_chat_message_contents:
|
||||
choice_index = streaming_chat_message_content.choice_index
|
||||
if choice_index not in all_messages:
|
||||
all_messages[choice_index] = []
|
||||
all_messages[choice_index].append(streaming_chat_message_content)
|
||||
yield streaming_chat_message_contents
|
||||
|
||||
all_messages_flattened = [
|
||||
reduce(lambda x, y: x + y, messages) for messages in all_messages.values()
|
||||
]
|
||||
_set_completion_response(current_span, all_messages_flattened, model_provider)
|
||||
except Exception as exception:
|
||||
_set_completion_error(current_span, exception)
|
||||
raise
|
||||
|
||||
# Mark the wrapper decorator as a streaming chat completion decorator
|
||||
wrapper_decorator.__model_diagnostics_streaming_chat_completion__ = True # type: ignore
|
||||
return wrapper_decorator
|
||||
|
||||
return inner_trace_streaming_chat_completion
|
||||
|
||||
|
||||
@experimental
|
||||
def trace_text_completion(model_provider: str) -> Callable:
|
||||
"""Decorator to trace text completion activities.
|
||||
|
||||
Args:
|
||||
model_provider (str): The model provider should describe a family of
|
||||
GenAI models with specific model identified by ai_model_id. For example,
|
||||
model_provider could be "openai" and ai_model_id could be "gpt-3.5-turbo".
|
||||
Sometimes the model provider is unknown at runtime, in which case it can be
|
||||
set to the most specific known provider. For example, while using local models
|
||||
hosted by Ollama, the model provider could be set to "ollama".
|
||||
"""
|
||||
|
||||
def inner_trace_text_completion(completion_func: Callable) -> Callable:
|
||||
@functools.wraps(completion_func)
|
||||
async def wrapper_decorator(*args: Any, **kwargs: Any) -> list[TextContent]:
|
||||
if not are_model_diagnostics_enabled():
|
||||
# If model diagnostics are not enabled, just return the completion
|
||||
return await completion_func(*args, **kwargs)
|
||||
|
||||
completion_service: "TextCompletionClientBase" = args[0]
|
||||
prompt: str = kwargs.get("prompt") if kwargs.get("prompt") is not None else args[1] # type: ignore
|
||||
settings: "PromptExecutionSettings" = kwargs["settings"] if kwargs.get("settings") is not None else args[2]
|
||||
|
||||
with use_span(
|
||||
_get_completion_span(
|
||||
TEXT_COMPLETION_OPERATION,
|
||||
completion_service.ai_model_id,
|
||||
model_provider,
|
||||
completion_service.service_url(),
|
||||
settings,
|
||||
),
|
||||
end_on_exit=True,
|
||||
) as current_span:
|
||||
_set_completion_input(model_provider, prompt)
|
||||
try:
|
||||
completions: list[TextContent] = await completion_func(*args, **kwargs)
|
||||
_set_completion_response(current_span, completions, model_provider)
|
||||
return completions
|
||||
except Exception as exception:
|
||||
_set_completion_error(current_span, exception)
|
||||
raise
|
||||
|
||||
# Mark the wrapper decorator as a text completion decorator
|
||||
wrapper_decorator.__model_diagnostics_text_completion__ = True # type: ignore
|
||||
|
||||
return wrapper_decorator
|
||||
|
||||
return inner_trace_text_completion
|
||||
|
||||
|
||||
@experimental
|
||||
def trace_streaming_text_completion(model_provider: str) -> Callable:
|
||||
"""Decorator to trace streaming text completion activities.
|
||||
|
||||
Args:
|
||||
model_provider (str): The model provider should describe a family of
|
||||
GenAI models with specific model identified by ai_model_id. For example,
|
||||
model_provider could be "openai" and ai_model_id could be "gpt-3.5-turbo".
|
||||
Sometimes the model provider is unknown at runtime, in which case it can be
|
||||
set to the most specific known provider. For example, while using local models
|
||||
hosted by Ollama, the model provider could be set to "ollama".
|
||||
"""
|
||||
|
||||
def inner_trace_streaming_text_completion(completion_func: Callable) -> Callable:
|
||||
@functools.wraps(completion_func)
|
||||
async def wrapper_decorator(*args: Any, **kwargs: Any) -> AsyncGenerator[list["StreamingTextContent"], Any]:
|
||||
if not are_model_diagnostics_enabled():
|
||||
# If model diagnostics are not enabled, just return the completion
|
||||
async for streaming_text_contents in completion_func(*args, **kwargs):
|
||||
yield streaming_text_contents
|
||||
return
|
||||
|
||||
completion_service: "TextCompletionClientBase" = args[0]
|
||||
prompt: str = kwargs.get("prompt") if kwargs.get("prompt") is not None else args[1] # type: ignore
|
||||
settings: "PromptExecutionSettings" = kwargs["settings"] if kwargs.get("settings") is not None else args[2]
|
||||
|
||||
all_text_contents: dict[int, list["StreamingTextContent"]] = {}
|
||||
|
||||
with use_span(
|
||||
_get_completion_span(
|
||||
TEXT_COMPLETION_OPERATION,
|
||||
completion_service.ai_model_id,
|
||||
model_provider,
|
||||
completion_service.service_url(),
|
||||
settings,
|
||||
),
|
||||
end_on_exit=True,
|
||||
) as current_span:
|
||||
_set_completion_input(model_provider, prompt)
|
||||
try:
|
||||
async for streaming_text_contents in completion_func(*args, **kwargs):
|
||||
for streaming_text_content in streaming_text_contents:
|
||||
choice_index = streaming_text_content.choice_index
|
||||
if choice_index not in all_text_contents:
|
||||
all_text_contents[choice_index] = []
|
||||
all_text_contents[choice_index].append(streaming_text_content)
|
||||
yield streaming_text_contents
|
||||
|
||||
all_text_contents_flattened = [
|
||||
reduce(lambda x, y: x + y, messages) for messages in all_text_contents.values()
|
||||
]
|
||||
_set_completion_response(current_span, all_text_contents_flattened, model_provider)
|
||||
except Exception as exception:
|
||||
_set_completion_error(current_span, exception)
|
||||
raise
|
||||
|
||||
# Mark the wrapper decorator as a streaming text completion decorator
|
||||
wrapper_decorator.__model_diagnostics_streaming_text_completion__ = True # type: ignore
|
||||
return wrapper_decorator
|
||||
|
||||
return inner_trace_streaming_text_completion
|
||||
|
||||
|
||||
def _get_completion_span(
|
||||
operation_name: str,
|
||||
model_name: str,
|
||||
model_provider: str,
|
||||
service_url: str | None,
|
||||
execution_settings: "PromptExecutionSettings | None",
|
||||
) -> Span:
|
||||
"""Start a text or chat completion span for a given model.
|
||||
|
||||
Note that `start_span` doesn't make the span the current span.
|
||||
Use `use_span` to make it the current span as a context manager.
|
||||
"""
|
||||
span = tracer.start_span(f"{operation_name} {model_name}")
|
||||
|
||||
# Set attributes on the span
|
||||
span.set_attributes({
|
||||
gen_ai_attributes.OPERATION: operation_name,
|
||||
gen_ai_attributes.SYSTEM: model_provider,
|
||||
gen_ai_attributes.MODEL: model_name,
|
||||
})
|
||||
|
||||
if service_url:
|
||||
span.set_attribute(gen_ai_attributes.ADDRESS, service_url)
|
||||
|
||||
# TODO(@glahaye): we'll need to have a way to get these attributes from model
|
||||
# providers other than OpenAI (for example if the attributes are named differently)
|
||||
if execution_settings:
|
||||
attribute_name_map = {
|
||||
"seed": gen_ai_attributes.SEED,
|
||||
"encoding_formats": gen_ai_attributes.ENCODING_FORMATS,
|
||||
"frequency_penalty": gen_ai_attributes.FREQUENCY_PENALTY,
|
||||
"max_tokens": gen_ai_attributes.MAX_TOKENS,
|
||||
"stop_sequences": gen_ai_attributes.STOP_SEQUENCES,
|
||||
"temperature": gen_ai_attributes.TEMPERATURE,
|
||||
"top_k": gen_ai_attributes.TOP_K,
|
||||
"top_p": gen_ai_attributes.TOP_P,
|
||||
}
|
||||
for attribute_name, attribute_key in attribute_name_map.items():
|
||||
attribute = execution_settings.extension_data.get(attribute_name)
|
||||
if attribute:
|
||||
span.set_attribute(attribute_key, attribute)
|
||||
|
||||
return span
|
||||
|
||||
|
||||
def _set_completion_input(
|
||||
model_provider: str,
|
||||
prompt: str | ChatHistory,
|
||||
) -> None:
|
||||
"""Set the input for a text or chat completion.
|
||||
|
||||
The logs will be associated to the current span.
|
||||
"""
|
||||
if are_sensitive_events_enabled():
|
||||
if isinstance(prompt, ChatHistory):
|
||||
for idx, message in enumerate(prompt.messages):
|
||||
event_name = gen_ai_attributes.ROLE_EVENT_MAP.get(message.role)
|
||||
if event_name:
|
||||
logger.info(
|
||||
json.dumps(message.to_dict()),
|
||||
extra={
|
||||
gen_ai_attributes.EVENT_NAME: event_name,
|
||||
gen_ai_attributes.SYSTEM: model_provider,
|
||||
ChatHistoryMessageTimestampFilter.INDEX_KEY: idx,
|
||||
},
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
prompt,
|
||||
extra={
|
||||
gen_ai_attributes.EVENT_NAME: gen_ai_attributes.PROMPT,
|
||||
gen_ai_attributes.SYSTEM: model_provider,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _set_completion_response(
|
||||
current_span: Span,
|
||||
completions: list[ChatMessageContent]
|
||||
| list[TextContent]
|
||||
| list[StreamingChatMessageContent]
|
||||
| list[StreamingTextContent],
|
||||
model_provider: str,
|
||||
) -> None:
|
||||
"""Set the a text or chat completion response for a given span."""
|
||||
first_completion = completions[0]
|
||||
|
||||
# Set the response ID
|
||||
response_id = first_completion.metadata.get("id")
|
||||
if response_id:
|
||||
current_span.set_attribute(gen_ai_attributes.RESPONSE_ID, response_id)
|
||||
|
||||
# Set the finish reason
|
||||
finish_reasons = [
|
||||
str(completion.finish_reason) for completion in completions if isinstance(completion, ChatMessageContent)
|
||||
]
|
||||
if finish_reasons:
|
||||
current_span.set_attribute(gen_ai_attributes.FINISH_REASON, ",".join(finish_reasons))
|
||||
|
||||
# Set usage attributes
|
||||
usage = first_completion.metadata.get("usage", None)
|
||||
if isinstance(usage, CompletionUsage):
|
||||
if usage.prompt_tokens:
|
||||
current_span.set_attribute(gen_ai_attributes.INPUT_TOKENS, usage.prompt_tokens)
|
||||
if usage.completion_tokens:
|
||||
current_span.set_attribute(gen_ai_attributes.OUTPUT_TOKENS, usage.completion_tokens)
|
||||
|
||||
# Set the completion event
|
||||
if are_sensitive_events_enabled():
|
||||
for completion in completions:
|
||||
full_response: dict[str, Any] = {
|
||||
"message": completion.to_dict(),
|
||||
}
|
||||
|
||||
if isinstance(completion, ChatMessageContent):
|
||||
full_response["finish_reason"] = completion.finish_reason
|
||||
if isinstance(completion, StreamingContentMixin):
|
||||
full_response["index"] = completion.choice_index
|
||||
|
||||
logger.info(
|
||||
json.dumps(full_response),
|
||||
extra={
|
||||
gen_ai_attributes.EVENT_NAME: gen_ai_attributes.CHOICE,
|
||||
gen_ai_attributes.SYSTEM: model_provider,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _set_completion_error(span: Span, error: Exception) -> None:
|
||||
"""Set an error for a text or chat completion ."""
|
||||
span.set_attribute(gen_ai_attributes.ERROR_TYPE, str(type(error)))
|
||||
span.set_status(StatusCode.ERROR, repr(error))
|
||||
@@ -0,0 +1,65 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from opentelemetry import trace
|
||||
|
||||
from semantic_kernel.utils.feature_stage_decorator import experimental
|
||||
from semantic_kernel.utils.telemetry.model_diagnostics.gen_ai_attributes import (
|
||||
OPERATION,
|
||||
TOOL_CALL_ID,
|
||||
TOOL_DESCRIPTION,
|
||||
TOOL_NAME,
|
||||
)
|
||||
from semantic_kernel.utils.telemetry.model_diagnostics.model_diagnostics_settings import ModelDiagnosticSettings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.functions.kernel_function import KernelFunction
|
||||
|
||||
|
||||
# The operation name is defined by OTeL GenAI semantic conventions:
|
||||
# https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/#execute-tool-span
|
||||
OPERATION_NAME = "execute_tool"
|
||||
|
||||
# To enable these features, set one of the following environment variables to true:
|
||||
# SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS
|
||||
# SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE
|
||||
MODEL_DIAGNOSTICS_SETTINGS = ModelDiagnosticSettings()
|
||||
|
||||
|
||||
@experimental
|
||||
def are_sensitive_events_enabled() -> bool:
|
||||
"""Check if sensitive events are enabled.
|
||||
|
||||
Sensitive events are enabled if the diagnostic with sensitive events is enabled.
|
||||
"""
|
||||
return MODEL_DIAGNOSTICS_SETTINGS.enable_otel_diagnostics_sensitive
|
||||
|
||||
|
||||
def start_as_current_span(
|
||||
tracer: trace.Tracer,
|
||||
function: "KernelFunction",
|
||||
metadata: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Starts a span for the given function using the provided tracer.
|
||||
|
||||
Args:
|
||||
tracer (trace.Tracer): The OpenTelemetry tracer to use.
|
||||
function (KernelFunction): The function for which to start the span.
|
||||
metadata (dict[str, Any] | None): Optional metadata to include in the span attributes.
|
||||
|
||||
Returns:
|
||||
trace.Span: The started span as a context manager.
|
||||
"""
|
||||
attributes = {
|
||||
OPERATION: OPERATION_NAME,
|
||||
TOOL_NAME: function.fully_qualified_name,
|
||||
}
|
||||
|
||||
tool_call_id = metadata.get("id", None) if metadata else None
|
||||
if tool_call_id:
|
||||
attributes[TOOL_CALL_ID] = tool_call_id
|
||||
if function.description:
|
||||
attributes[TOOL_DESCRIPTION] = function.description
|
||||
|
||||
return tracer.start_as_current_span(f"{OPERATION_NAME} {function.fully_qualified_name}", attributes=attributes)
|
||||
@@ -0,0 +1,53 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from semantic_kernel.contents.utils.author_role import AuthorRole
|
||||
|
||||
# Constants for tracing activities with semantic conventions.
|
||||
# Ideally, we should use the attributes from the semcov package.
|
||||
# However, many of the attributes are not yet available in the package,
|
||||
# so we define them here for now.
|
||||
|
||||
# Activity tags
|
||||
OPERATION = "gen_ai.operation.name"
|
||||
SYSTEM = "gen_ai.system"
|
||||
ERROR_TYPE = "error.type"
|
||||
MODEL = "gen_ai.request.model"
|
||||
SEED = "gen_ai.request.seed"
|
||||
PORT = "server.port"
|
||||
ENCODING_FORMATS = "gen_ai.request.encoding_formats"
|
||||
FREQUENCY_PENALTY = "gen_ai.request.frequency_penalty"
|
||||
MAX_TOKENS = "gen_ai.request.max_tokens"
|
||||
STOP_SEQUENCES = "gen_ai.request.stop_sequences"
|
||||
TEMPERATURE = "gen_ai.request.temperature"
|
||||
TOP_K = "gen_ai.request.top_k"
|
||||
TOP_P = "gen_ai.request.top_p"
|
||||
FINISH_REASON = "gen_ai.response.finish_reason"
|
||||
RESPONSE_ID = "gen_ai.response.id"
|
||||
INPUT_TOKENS = "gen_ai.usage.input_tokens"
|
||||
OUTPUT_TOKENS = "gen_ai.usage.output_tokens"
|
||||
TOOL_CALL_ID = "gen_ai.tool.call.id"
|
||||
TOOL_CALL_ARGUMENTS = "gen_ai.tool.call.arguments"
|
||||
TOOL_CALL_RESULT = "gen_ai.tool.call.result"
|
||||
TOOL_DESCRIPTION = "gen_ai.tool.description"
|
||||
TOOL_NAME = "gen_ai.tool.name"
|
||||
ADDRESS = "server.address"
|
||||
|
||||
# Activity events
|
||||
EVENT_NAME = "event.name"
|
||||
SYSTEM_MESSAGE = "gen_ai.system.message"
|
||||
USER_MESSAGE = "gen_ai.user.message"
|
||||
ASSISTANT_MESSAGE = "gen_ai.assistant.message"
|
||||
TOOL_MESSAGE = "gen_ai.tool.message"
|
||||
CHOICE = "gen_ai.choice"
|
||||
PROMPT = "gen_ai.prompt"
|
||||
|
||||
# Kernel specific attributes
|
||||
AVAILABLE_FUNCTIONS = "sk.available_functions"
|
||||
|
||||
|
||||
ROLE_EVENT_MAP = {
|
||||
AuthorRole.SYSTEM: SYSTEM_MESSAGE,
|
||||
AuthorRole.USER: USER_MESSAGE,
|
||||
AuthorRole.ASSISTANT: ASSISTANT_MESSAGE,
|
||||
AuthorRole.TOOL: TOOL_MESSAGE,
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from typing import ClassVar
|
||||
|
||||
from semantic_kernel.kernel_pydantic import KernelBaseSettings
|
||||
from semantic_kernel.utils.feature_stage_decorator import experimental
|
||||
|
||||
|
||||
@experimental
|
||||
class ModelDiagnosticSettings(KernelBaseSettings):
|
||||
"""Settings for model diagnostics.
|
||||
|
||||
The settings are first loaded from environment variables with
|
||||
the prefix 'SEMANTICKERNEL_EXPERIMENTAL_GENAI_'.
|
||||
If the environment variables are not found, the settings can
|
||||
be loaded from a .env file with the encoding 'utf-8'.
|
||||
If the settings are not found in the .env file, the settings
|
||||
are ignored; however, validation will fail alerting that the
|
||||
settings are missing.
|
||||
|
||||
Required settings for prefix 'SEMANTICKERNEL_EXPERIMENTAL_GENAI_' are:
|
||||
- enable_otel_diagnostics: bool - Enable OpenTelemetry diagnostics. Default is False.
|
||||
(Env var SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS)
|
||||
- enable_otel_diagnostics_sensitive: bool - Enable OpenTelemetry sensitive events. Default is False.
|
||||
(Env var SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE)
|
||||
"""
|
||||
|
||||
env_prefix: ClassVar[str] = "SEMANTICKERNEL_EXPERIMENTAL_GENAI_"
|
||||
|
||||
enable_otel_diagnostics: bool = False
|
||||
enable_otel_diagnostics_sensitive: bool = False
|
||||
@@ -0,0 +1,46 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import os
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
from typing import Any
|
||||
|
||||
from semantic_kernel.const import USER_AGENT
|
||||
|
||||
# Note that if this environment variable does not exist, telemetry is enabled.
|
||||
TELEMETRY_DISABLED_ENV_VAR = "AZURE_TELEMETRY_DISABLED"
|
||||
|
||||
IS_TELEMETRY_ENABLED = os.environ.get(TELEMETRY_DISABLED_ENV_VAR, "false").lower() not in ["true", "1"]
|
||||
|
||||
HTTP_USER_AGENT = "semantic-kernel-python"
|
||||
|
||||
try:
|
||||
version_info = version("semantic-kernel")
|
||||
except PackageNotFoundError:
|
||||
version_info = "dev"
|
||||
|
||||
APP_INFO = (
|
||||
{
|
||||
"semantic-kernel-version": f"python/{version_info}",
|
||||
}
|
||||
if IS_TELEMETRY_ENABLED
|
||||
else None
|
||||
)
|
||||
|
||||
|
||||
SEMANTIC_KERNEL_USER_AGENT = f"{HTTP_USER_AGENT}/{version_info}"
|
||||
|
||||
|
||||
def prepend_semantic_kernel_to_user_agent(headers: dict[str, Any]):
|
||||
"""Prepend "semantic-kernel" to the User-Agent in the headers.
|
||||
|
||||
Args:
|
||||
headers: The existing headers dictionary.
|
||||
|
||||
Returns:
|
||||
The modified headers dictionary with "semantic-kernel-python/{version}" prepended to the User-Agent.
|
||||
"""
|
||||
headers[USER_AGENT] = (
|
||||
f"{SEMANTIC_KERNEL_USER_AGENT} {headers[USER_AGENT]}" if USER_AGENT in headers else SEMANTIC_KERNEL_USER_AGENT
|
||||
)
|
||||
|
||||
return headers
|
||||
@@ -0,0 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
AGENT_NAME_REGEX = r"^[0-9A-Za-z_-]+$"
|
||||
PLUGIN_NAME_REGEX = r"^[0-9A-Za-z_]+$"
|
||||
FUNCTION_NAME_REGEX = r"^[0-9A-Za-z_-]+$"
|
||||
FULLY_QUALIFIED_FUNCTION_NAME = r"^(?P<plugin>[0-9A-Za-z_]+)[.](?P<function>[0-9A-Za-z_-]+)$"
|
||||
FUNCTION_PARAM_NAME_REGEX = r"^[0-9A-Za-z_-]+$"
|
||||
Reference in New Issue
Block a user