chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -0,0 +1,23 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.azure_ai_inference.azure_ai_inference_prompt_execution_settings import (
AzureAIInferenceChatPromptExecutionSettings,
AzureAIInferenceEmbeddingPromptExecutionSettings,
AzureAIInferencePromptExecutionSettings,
)
from semantic_kernel.connectors.ai.azure_ai_inference.azure_ai_inference_settings import AzureAIInferenceSettings
from semantic_kernel.connectors.ai.azure_ai_inference.services.azure_ai_inference_chat_completion import (
AzureAIInferenceChatCompletion,
)
from semantic_kernel.connectors.ai.azure_ai_inference.services.azure_ai_inference_text_embedding import (
AzureAIInferenceTextEmbedding,
)
__all__ = [
"AzureAIInferenceChatCompletion",
"AzureAIInferenceChatPromptExecutionSettings",
"AzureAIInferenceEmbeddingPromptExecutionSettings",
"AzureAIInferencePromptExecutionSettings",
"AzureAIInferenceSettings",
"AzureAIInferenceTextEmbedding",
]
@@ -0,0 +1,100 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Annotated, Any, Literal
from pydantic import BaseModel, Field, model_validator
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.exceptions.service_exceptions import ServiceInvalidExecutionSettingsError
from semantic_kernel.utils.feature_stage_decorator import experimental
@experimental
class AzureAIInferencePromptExecutionSettings(PromptExecutionSettings):
"""Azure AI Inference Prompt Execution Settings.
Note:
`extra_parameters` is a dictionary to pass additional model-specific parameters to the model.
"""
frequency_penalty: Annotated[float | None, Field(ge=-2.0, le=2.0)] = None
max_tokens: Annotated[int | None, Field(gt=0)] = None
presence_penalty: Annotated[float | None, Field(ge=-2.0, le=2.0)] = None
seed: int | None = None
stop: str | None = None
temperature: Annotated[float | None, Field(ge=0.0, le=1.0)] = None
top_p: Annotated[float | None, Field(ge=0.0, le=1.0)] = None
extra_parameters: dict[str, Any] | None = None
@experimental
class AzureAIInferenceChatPromptExecutionSettings(AzureAIInferencePromptExecutionSettings):
"""Azure AI Inference Chat Prompt Execution Settings."""
response_format: (
dict[Literal["type"], Literal["text", "json_object"]] | dict[str, Any] | type[BaseModel] | type | None
) = None
structured_json_response: Annotated[
bool, Field(description="Do not set this manually. It is set by the service.")
] = False
tools: Annotated[
list[dict[str, Any]] | None,
Field(
description="Do not set this manually. It is set by the service based "
"on the function choice configuration.",
),
] = None
tool_choice: Annotated[
str | None,
Field(
description="Do not set this manually. It is set by the service based "
"on the function choice configuration.",
),
] = None
@model_validator(mode="before")
def validate_response_format_and_set_flag(cls, values: Any) -> Any:
"""Validate the response_format and set structured_json_response accordingly."""
if not isinstance(values, dict):
return values
response_format = values.get("response_format", None)
if response_format is None:
return values
if isinstance(response_format, dict):
if response_format.get("type") == "json_object":
return values
if response_format.get("type") == "json_schema":
json_schema = response_format.get("json_schema")
if isinstance(json_schema, dict):
values["structured_json_response"] = True
return values
raise ServiceInvalidExecutionSettingsError(
"If response_format has type 'json_schema', 'json_schema' must be a valid dictionary."
)
if isinstance(response_format, type):
if issubclass(response_format, BaseModel):
values["structured_json_response"] = True
else:
values["structured_json_response"] = True
else:
raise ServiceInvalidExecutionSettingsError(
"response_format must be a dictionary, a subclass of BaseModel, a Python class/type, or None"
)
return values
@experimental
class AzureAIInferenceEmbeddingPromptExecutionSettings(PromptExecutionSettings):
"""Azure AI Inference Embedding Prompt Execution Settings.
Note:
`extra_parameters` is a dictionary to pass additional model-specific parameters to the model.
"""
dimensions: Annotated[int | None, Field(gt=0)] = None
encoding_format: Literal["base64", "binary", "float", "int8", "ubinary", "uint8"] | None = None
input_type: Literal["text", "query", "document"] | None = None
extra_parameters: dict[str, str] | None = None
@@ -0,0 +1,41 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import ClassVar
from pydantic import SecretStr
from semantic_kernel.connectors.ai.open_ai.const import DEFAULT_AZURE_API_VERSION
from semantic_kernel.kernel_pydantic import HttpsUrl, KernelBaseSettings
from semantic_kernel.utils.feature_stage_decorator import experimental
@experimental
class AzureAIInferenceSettings(KernelBaseSettings):
"""Azure AI Inference settings.
The settings are first loaded from environment variables with
the prefix 'AZURE_AI_INFERENCE_'.
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 'AZURE_AI_INFERENCE_' are:
- endpoint: HttpsUrl - The endpoint of the Azure AI Inference service deployment.
This value can be found in the Keys & Endpoint section when examining
your resource from the Azure portal.
(Env var AZURE_AI_INFERENCE_ENDPOINT)
- api_key: SecretStr - The API key for the Azure AI Inference service deployment.
This value can be found in the Keys & Endpoint section when examining
your resource from the Azure portal. You can use either KEY1 or KEY2.
(Env var AZURE_AI_INFERENCE_API_KEY)
- api_version: str | None - The API version to use. The default value is "2024-10-21".
(Env var AZURE_AI_INFERENCE_API_VERSION)
"""
env_prefix: ClassVar[str] = "AZURE_AI_INFERENCE_"
endpoint: HttpsUrl
api_key: SecretStr | None = None
api_version: str = DEFAULT_AZURE_API_VERSION
@@ -0,0 +1 @@
# Copyright (c) Microsoft. All rights reserved.
@@ -0,0 +1,128 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import contextlib
from abc import ABC
from enum import Enum
from typing import Any
from azure.ai.inference.aio import ChatCompletionsClient, EmbeddingsClient
from azure.core.credentials import AzureKeyCredential
from azure.core.credentials_async import AsyncTokenCredential
from pydantic import ValidationError
from semantic_kernel.connectors.ai.azure_ai_inference.azure_ai_inference_settings import AzureAIInferenceSettings
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.utils.feature_stage_decorator import experimental
from semantic_kernel.utils.telemetry.user_agent import SEMANTIC_KERNEL_USER_AGENT
class AzureAIInferenceClientType(Enum):
"""Client type for Azure AI Inference."""
ChatCompletions = "ChatCompletions"
Embeddings = "Embeddings"
@classmethod
def get_client_class(cls, client_type: "AzureAIInferenceClientType") -> Any:
"""Get the client class based on the client type."""
class_mapping = {
cls.ChatCompletions: ChatCompletionsClient,
cls.Embeddings: EmbeddingsClient,
}
return class_mapping[client_type]
@experimental
class AzureAIInferenceBase(KernelBaseModel, ABC):
"""Azure AI Inference Chat Completion Service."""
client: ChatCompletionsClient | EmbeddingsClient
managed_client: bool = False
def __init__(
self,
client_type: AzureAIInferenceClientType,
api_key: str | None = None,
endpoint: str | None = None,
api_version: str | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
client: ChatCompletionsClient | EmbeddingsClient | None = None,
instruction_role: str | None = None,
credential: AsyncTokenCredential | None = None,
**kwargs: Any,
) -> None:
"""Initialize the Azure AI Inference Chat Completion service.
If no arguments are provided, the service will attempt to load the settings from the environment.
The following environment variables are used:
- AZURE_AI_INFERENCE_API_KEY
- AZURE_AI_INFERENCE_ENDPOINT
- AZURE_AI_INFERENCE_API_VERSION
Args:
client_type (AzureAIInferenceClientType): The client type to use.
api_key (str | None): The API key for the Azure AI Inference service deployment. (Optional)
endpoint (str | None): The endpoint of the Azure AI Inference service deployment. (Optional)
api_version (str | None): The API version to use. (Optional)
env_file_path (str | None): The path to the environment file. (Optional)
env_file_encoding (str | None): The encoding of the environment file. (Optional)
client (ChatCompletionsClient | None): The Azure AI Inference client to use. (Optional)
instruction_role (str | None): The role to use for 'instruction' messages. (Optional)
credential: The credential to use for authentication. (Optional)
**kwargs: Additional keyword arguments.
Raises:
ServiceInitializationError: If an error occurs during initialization.
"""
managed_client = client is None
if not client:
try:
azure_ai_inference_settings = AzureAIInferenceSettings(
api_key=api_key,
endpoint=endpoint,
api_version=api_version,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as e:
raise ServiceInitializationError(f"Failed to validate Azure AI Inference settings: {e}") from e
endpoint = str(azure_ai_inference_settings.endpoint)
if azure_ai_inference_settings.api_key is not None:
client = AzureAIInferenceClientType.get_client_class(client_type)(
endpoint=endpoint,
credential=AzureKeyCredential(azure_ai_inference_settings.api_key.get_secret_value()),
user_agent=SEMANTIC_KERNEL_USER_AGENT,
api_version=azure_ai_inference_settings.api_version,
)
else:
if credential is None:
raise ServiceInitializationError("The 'credential' parameter is required for authentication.")
client = AzureAIInferenceClientType.get_client_class(client_type)(
endpoint=endpoint,
credential=credential,
user_agent=SEMANTIC_KERNEL_USER_AGENT,
api_version=azure_ai_inference_settings.api_version,
)
args: dict[str, Any] = {
"client": client,
"managed_client": managed_client,
**kwargs,
}
if instruction_role:
args["instruction_role"] = instruction_role
super().__init__(**args)
def __del__(self) -> None:
"""Close the client when the object is deleted."""
if self.managed_client:
with contextlib.suppress(Exception):
asyncio.get_running_loop().create_task(self.client.close())
@@ -0,0 +1,395 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
import sys
from collections.abc import AsyncGenerator, Callable
from typing import TYPE_CHECKING, Any, ClassVar
from azure.ai.inference.aio import ChatCompletionsClient
from azure.ai.inference.models import (
AsyncStreamingChatCompletions,
ChatChoice,
ChatCompletions,
ChatCompletionsToolCall,
ChatRequestMessage,
JsonSchemaFormat,
StreamingChatChoiceUpdate,
StreamingChatCompletionsUpdate,
StreamingChatResponseToolCallUpdate,
)
from pydantic import BaseModel
from semantic_kernel.connectors.ai.azure_ai_inference import AzureAIInferenceChatPromptExecutionSettings
from semantic_kernel.connectors.ai.azure_ai_inference.services.azure_ai_inference_base import (
AzureAIInferenceBase,
AzureAIInferenceClientType,
)
from semantic_kernel.connectors.ai.azure_ai_inference.services.azure_ai_inference_tracing import AzureAIInferenceTracing
from semantic_kernel.connectors.ai.azure_ai_inference.services.utils import MESSAGE_CONVERTERS
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.connectors.ai.completion_usage import CompletionUsage
from semantic_kernel.connectors.ai.function_calling_utils import update_settings_from_function_call_configuration
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import CMC_ITEM_TYPES, ChatMessageContent
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.streaming_chat_message_content import STREAMING_CMC_ITEM_TYPES as STREAMING_ITEM_TYPES
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.contents.utils.finish_reason import FinishReason
from semantic_kernel.exceptions.service_exceptions import ServiceInvalidExecutionSettingsError
from semantic_kernel.schema.kernel_json_schema_builder import KernelJsonSchemaBuilder
from semantic_kernel.utils.feature_stage_decorator import experimental
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
logger: logging.Logger = logging.getLogger(__name__)
@experimental
class AzureAIInferenceChatCompletion(ChatCompletionClientBase, AzureAIInferenceBase):
"""Azure AI Inference Chat Completion Service."""
SUPPORTS_FUNCTION_CALLING: ClassVar[bool] = True
def __init__(
self,
ai_model_id: str,
api_key: str | None = None,
endpoint: str | None = None,
api_version: str | None = None,
service_id: str | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
client: ChatCompletionsClient | None = None,
instruction_role: str | None = None,
) -> None:
"""Initialize the Azure AI Inference Chat Completion service.
If no arguments are provided, the service will attempt to load the settings from the environment.
The following environment variables are used:
- AZURE_AI_INFERENCE_API_KEY
- AZURE_AI_INFERENCE_ENDPOINT
- AZURE_AI_INFERENCE_API_VERSION
Args:
ai_model_id: (str): A string that is used to identify the model such as the model name. (Required)
api_key (str | None): The API key for the Azure AI Inference service deployment. (Optional)
endpoint (str | None): The endpoint of the Azure AI Inference service deployment. (Optional)
api_version (str | None): The API version to use. (Optional)
service_id (str | None): Service ID for the chat completion service. (Optional)
env_file_path (str | None): The path to the environment file. (Optional)
env_file_encoding (str | None): The encoding of the environment file. (Optional)
client (ChatCompletionsClient | None): The Azure AI Inference client to use. (Optional)
instruction_role (str | None): The role to use for 'instruction' messages, for example, summarization
prompts could use `developer` or `system`. (Optional)
Raises:
ServiceInitializationError: If an error occurs during initialization.
"""
args: dict[str, Any] = {
"ai_model_id": ai_model_id,
"api_key": api_key,
"client_type": AzureAIInferenceClientType.ChatCompletions,
"client": client,
"endpoint": endpoint,
"api_version": api_version,
"env_file_path": env_file_path,
"env_file_encoding": env_file_encoding,
}
if service_id:
args["service_id"] = service_id
if instruction_role:
args["instruction_role"] = instruction_role
super().__init__(**args)
# region Overriding base class methods
# Override from AIServiceClientBase
@override
def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]:
return AzureAIInferenceChatPromptExecutionSettings
# Override from AIServiceClientBase
@override
def service_url(self) -> str | None:
if hasattr(self.client, "_client") and hasattr(self.client._client, "_base_url"):
# Best effort to get the endpoint
return self.client._client._base_url
return None
@override
async def _inner_get_chat_message_contents(
self,
chat_history: "ChatHistory",
settings: "PromptExecutionSettings",
) -> list["ChatMessageContent"]:
if not isinstance(settings, AzureAIInferenceChatPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, AzureAIInferenceChatPromptExecutionSettings) # nosec
assert isinstance(self.client, ChatCompletionsClient) # nosec
with AzureAIInferenceTracing():
settings_dict = settings.prepare_settings_dict()
# Remove the extra parameters since it will be passed in via the `model_extras` param
settings_dict.pop("extra_parameters", None)
self._handle_structured_output(settings, settings_dict)
response: ChatCompletions = await self.client.complete(
messages=self._prepare_chat_history_for_request(chat_history),
# The model id will be ignored by the service if the endpoint serves only one model (i.e. MaaS)
model=self.ai_model_id,
model_extras=settings.extra_parameters,
**settings_dict,
)
response_metadata = self._get_metadata_from_response(response)
return [self._create_chat_message_content(response, choice, response_metadata) for choice in response.choices]
@override
async def _inner_get_streaming_chat_message_contents(
self,
chat_history: "ChatHistory",
settings: "PromptExecutionSettings",
function_invoke_attempt: int = 0,
) -> AsyncGenerator[list["StreamingChatMessageContent"], Any]:
if not isinstance(settings, AzureAIInferenceChatPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, AzureAIInferenceChatPromptExecutionSettings) # nosec
assert isinstance(self.client, ChatCompletionsClient) # nosec
with AzureAIInferenceTracing():
settings_dict = settings.prepare_settings_dict()
# Remove the extra parameters since it will be passed in via the `model_extras` param
settings_dict.pop("extra_parameters", None)
self._handle_structured_output(settings, settings_dict)
response: AsyncStreamingChatCompletions = await self.client.complete(
stream=True,
# The model id will be ignored by the service if the endpoint serves only one model (i.e. MaaS)
model=self.ai_model_id,
messages=self._prepare_chat_history_for_request(chat_history),
model_extras=settings.extra_parameters,
**settings_dict,
)
async for chunk in response:
if len(chunk.choices) == 0:
continue
chunk_metadata = self._get_metadata_from_response(chunk)
yield [
self._create_streaming_chat_message_content(chunk, choice, chunk_metadata, function_invoke_attempt)
for choice in chunk.choices
]
@override
def _verify_function_choice_settings(self, settings: "PromptExecutionSettings") -> None:
if not isinstance(settings, AzureAIInferenceChatPromptExecutionSettings):
raise ServiceInvalidExecutionSettingsError(
"The settings must be an AzureAIInferenceChatPromptExecutionSettings."
)
if settings.extra_parameters is not None and settings.extra_parameters.get("n", 1) > 1:
# Currently only OpenAI models allow multiple completions but the Azure AI Inference service
# does not expose the functionality directly. If users want to have more than 1 responses, they
# need to configure `extra_parameters` with a key of "n" and a value greater than 1.
raise ServiceInvalidExecutionSettingsError(
"Auto invocation of tool calls may only be used with a single completion."
)
@override
def _update_function_choice_settings_callback(
self,
) -> Callable[["FunctionCallChoiceConfiguration", "PromptExecutionSettings", FunctionChoiceType], None]:
return update_settings_from_function_call_configuration
@override
def _reset_function_choice_settings(self, settings: "PromptExecutionSettings") -> None:
if hasattr(settings, "tool_choice"):
settings.tool_choice = None
if hasattr(settings, "tools"):
settings.tools = None
@override
def _prepare_chat_history_for_request(
self,
chat_history: ChatHistory,
role_key: str = "role",
content_key: str = "content",
) -> list[ChatRequestMessage]:
chat_request_messages: list[ChatRequestMessage] = []
for message in chat_history.messages:
# If instruction_role is 'developer' and the message role is 'system', change it to 'developer'
role = (
AuthorRole.DEVELOPER
if self.instruction_role == "developer" and message.role == AuthorRole.SYSTEM
else message.role
)
chat_request_messages.append(MESSAGE_CONVERTERS[role](message))
return chat_request_messages
def _handle_structured_output(
self, request_settings: AzureAIInferenceChatPromptExecutionSettings, settings: dict[str, Any]
) -> None:
response_format = getattr(request_settings, "response_format", None)
if getattr(request_settings, "structured_json_response", False) and response_format:
# Case 1: response_format is a Pydantic BaseModel type
if isinstance(response_format, type) and issubclass(response_format, BaseModel):
schema = response_format.model_json_schema()
settings["response_format"] = JsonSchemaFormat(
name=response_format.__name__,
schema=schema,
description=f"Schema for {response_format.__name__}",
strict=True,
)
# Case 2: response_format is a type but not a subclass of BaseModel
elif isinstance(response_format, type):
generated_schema = KernelJsonSchemaBuilder.build(parameter_type=response_format, structured_output=True)
assert generated_schema is not None # nosec
settings["response_format"] = JsonSchemaFormat(
name=response_format.__name__,
schema=generated_schema,
description=f"Schema for {response_format.__name__}",
strict=True,
)
# Case 3: response_format is already a JsonSchemaFormat instance, pass it
elif isinstance(response_format, JsonSchemaFormat):
settings["response_format"] = response_format
# Case 4: response_format is a dictionary (legacy), create JsonSchemaFormat from dict
elif isinstance(response_format, dict):
settings["response_format"] = JsonSchemaFormat(**response_format)
# endregion
# region Non-streaming
def _create_chat_message_content(
self, response: ChatCompletions, choice: ChatChoice, metadata: dict[str, Any]
) -> ChatMessageContent:
"""Create a chat message content object.
Args:
response: The response from the service.
choice: The choice from the response.
metadata: The metadata from the response.
Returns:
A chat message content object.
"""
items: list[CMC_ITEM_TYPES] = []
if choice.message.content:
items.append(
TextContent(
text=choice.message.content,
metadata=metadata,
)
)
if choice.message.tool_calls:
for tool_call in choice.message.tool_calls:
if isinstance(tool_call, ChatCompletionsToolCall):
items.append(
FunctionCallContent(
id=tool_call.id,
name=tool_call.function.name,
arguments=tool_call.function.arguments,
)
)
return ChatMessageContent(
role=AuthorRole(choice.message.role),
items=items,
inner_content=response,
finish_reason=FinishReason(choice.finish_reason) if choice.finish_reason else None,
metadata=metadata,
)
# endregion
# region Streaming
def _create_streaming_chat_message_content(
self,
chunk: AsyncStreamingChatCompletions,
choice: StreamingChatChoiceUpdate,
metadata: dict[str, Any],
function_invoke_attempt: int,
) -> StreamingChatMessageContent:
"""Create a streaming chat message content object.
Args:
chunk: The chunk from the response.
choice: The choice from the response.
metadata: The metadata from the response.
function_invoke_attempt: The function invoke attempt.
Returns:
A streaming chat message content object.
"""
items: list[STREAMING_ITEM_TYPES] = []
if choice.delta.content:
items.append(
StreamingTextContent(
choice_index=choice.index,
text=choice.delta.content,
inner_content=chunk,
metadata=metadata,
)
)
if choice.delta.tool_calls:
for tool_call in choice.delta.tool_calls:
if isinstance(tool_call, StreamingChatResponseToolCallUpdate):
items.append(
FunctionCallContent(
id=tool_call.id,
index=choice.index,
name=tool_call.function.name,
arguments=tool_call.function.arguments,
)
)
return StreamingChatMessageContent(
role=(AuthorRole(choice.delta.role) if choice.delta and choice.delta.role else AuthorRole.ASSISTANT),
items=items,
choice_index=choice.index,
inner_content=chunk,
finish_reason=FinishReason(choice.finish_reason) if choice.finish_reason else None,
metadata=metadata,
function_invoke_attempt=function_invoke_attempt,
ai_model_id=self.ai_model_id,
)
# endregion
def _get_metadata_from_response(self, response: ChatCompletions | StreamingChatCompletionsUpdate) -> dict[str, Any]:
"""Get metadata from the response.
Args:
response: The response from the service.
Returns:
A dictionary containing metadata.
"""
return {
"id": response.id,
"model": response.model,
"created": response.created,
"usage": CompletionUsage(
prompt_tokens=response.usage.prompt_tokens,
completion_tokens=response.usage.completion_tokens,
)
if response.usage
else None,
}
@@ -0,0 +1,108 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from typing import TYPE_CHECKING, Any
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
from azure.ai.inference.aio import EmbeddingsClient
from azure.ai.inference.models import EmbeddingsResult
from numpy import array, ndarray
from semantic_kernel.connectors.ai.azure_ai_inference.azure_ai_inference_prompt_execution_settings import (
AzureAIInferenceEmbeddingPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.azure_ai_inference.services.azure_ai_inference_base import (
AzureAIInferenceBase,
AzureAIInferenceClientType,
)
from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase
from semantic_kernel.utils.feature_stage_decorator import experimental
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
@experimental
class AzureAIInferenceTextEmbedding(EmbeddingGeneratorBase, AzureAIInferenceBase):
"""Azure AI Inference Text Embedding Service."""
def __init__(
self,
ai_model_id: str,
api_key: str | None = None,
endpoint: str | None = None,
api_version: str | None = None,
service_id: str | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
client: EmbeddingsClient | None = None,
) -> None:
"""Initialize the Azure AI Inference Text Embedding service.
If no arguments are provided, the service will attempt to load the settings from the environment.
The following environment variables are used:
- AZURE_AI_INFERENCE_API_KEY
- AZURE_AI_INFERENCE_ENDPOINT
- AZURE_AI_INFERENCE_API_VERSION
Args:
ai_model_id: (str): A string that is used to identify the model such as the model name. (Required)
api_key (str | None): The API key for the Azure AI Inference service deployment. (Optional)
endpoint (str | None): The endpoint of the Azure AI Inference service deployment. (Optional)
api_version (str | None): The API version to use. (Optional)
service_id (str | None): Service ID for the chat completion service. (Optional)
env_file_path (str | None): The path to the environment file. (Optional)
env_file_encoding (str | None): The encoding of the environment file. (Optional)
client (EmbeddingsClient | None): The Azure AI Inference client to use. (Optional)
Raises:
ServiceInitializationError: If an error occurs during initialization.
"""
super().__init__(
ai_model_id=ai_model_id,
service_id=service_id or ai_model_id,
client_type=AzureAIInferenceClientType.Embeddings,
api_key=api_key,
endpoint=endpoint,
api_version=api_version,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
client=client,
)
async def generate_embeddings(
self,
texts: list[str],
settings: "PromptExecutionSettings | None" = None,
**kwargs: Any,
) -> ndarray:
"""Generate embeddings from the Azure AI Inference service."""
if not settings:
settings = AzureAIInferenceEmbeddingPromptExecutionSettings()
else:
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, AzureAIInferenceEmbeddingPromptExecutionSettings) # nosec
assert isinstance(self.client, EmbeddingsClient) # nosec
response: EmbeddingsResult = await self.client.embed(
input=texts,
# The model id will be ignored by the service if the endpoint serves only one model (i.e. MaaS)
model=self.ai_model_id,
model_extras=settings.extra_parameters if settings else None,
dimensions=settings.dimensions if settings else None,
encoding_format=settings.encoding_format if settings else None,
input_type=settings.input_type if settings else None,
)
return array([array(item.embedding) for item in response.data])
@override
def get_prompt_execution_settings_class(
self,
) -> type["PromptExecutionSettings"]:
"""Get the request settings class."""
return AzureAIInferenceEmbeddingPromptExecutionSettings
@@ -0,0 +1,54 @@
# Copyright (c) Microsoft. All rights reserved.
from azure.ai.inference.tracing import AIInferenceInstrumentor
from azure.core.settings import settings
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.utils.telemetry.model_diagnostics.model_diagnostics_settings import ModelDiagnosticSettings
class AzureAIInferenceTracing(KernelBaseModel):
"""Enable tracing for Azure AI Inference.
This class is intended to be used as a context manager.
The instrument() call effect should be scoped to the context manager.
"""
diagnostics_settings: ModelDiagnosticSettings
def __init__(self, diagnostics_settings: ModelDiagnosticSettings | None = None) -> None:
"""Initialize the Azure AI Inference Tracing.
Args:
diagnostics_settings (ModelDiagnosticSettings, optional): Model diagnostics settings. Defaults to None.
"""
super().__init__(diagnostics_settings=diagnostics_settings or ModelDiagnosticSettings())
# Only set tracing implementation when diagnostics is enabled to avoid
# interfering with method mocking in tests
if (
self.diagnostics_settings.enable_otel_diagnostics
or self.diagnostics_settings.enable_otel_diagnostics_sensitive
):
settings.tracing_implementation = "opentelemetry"
def __enter__(self) -> None:
"""Enable tracing.
Both enable_otel_diagnostics and enable_otel_diagnostics_sensitive will enable tracing.
enable_otel_diagnostics_sensitive will also enable content recording.
"""
if (
self.diagnostics_settings.enable_otel_diagnostics
or self.diagnostics_settings.enable_otel_diagnostics_sensitive
):
AIInferenceInstrumentor().instrument( # type: ignore
enable_content_recording=self.diagnostics_settings.enable_otel_diagnostics_sensitive
)
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
"""Disable tracing."""
if (
self.diagnostics_settings.enable_otel_diagnostics
or self.diagnostics_settings.enable_otel_diagnostics_sensitive
):
AIInferenceInstrumentor().uninstrument()
@@ -0,0 +1,158 @@
# Copyright (c) Microsoft. All rights reserved.
import json
import logging
from collections.abc import Callable, Mapping
from azure.ai.inference.models import (
AssistantMessage,
ChatCompletionsToolCall,
ChatRequestMessage,
ContentItem,
FunctionCall,
ImageContentItem,
ImageDetailLevel,
ImageUrl,
SystemMessage,
TextContentItem,
ToolMessage,
UserMessage,
)
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.contents.image_content import ImageContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.contents.utils.author_role import AuthorRole
logger: logging.Logger = logging.getLogger(__name__)
def _format_system_message(message: ChatMessageContent) -> SystemMessage:
"""Format a system message to the expected object for the client.
Args:
message: The system message.
Returns:
The formatted system message.
"""
return SystemMessage(content=message.content)
def _format_developer_message(message: ChatMessageContent) -> ChatRequestMessage:
"""Format a developer message to the expected object for the client.
Args:
message: The developer message.
Returns:
The formatted developer message.
"""
return ChatRequestMessage({"role": "developer", "content": message.content})
def _format_user_message(message: ChatMessageContent) -> UserMessage:
"""Format a user message to the expected object for the client.
If there are any image items in the message, we need to create a list of content items,
otherwise we need to just pass in the content as a string or it will error.
Args:
message: The user message.
Returns:
The formatted user message.
"""
if not any(isinstance(item, (ImageContent)) for item in message.items):
return UserMessage(content=message.content)
content_items: list[ContentItem] = []
for item in message.items:
if isinstance(item, TextContent):
content_items.append(TextContentItem(text=item.text))
elif isinstance(item, ImageContent) and (item.data_uri or item.uri):
content_items.append(
ImageContentItem(
image_url=ImageUrl(url=item.data_uri or str(item.uri), detail=ImageDetailLevel.Auto.value)
)
)
else:
logger.warning(
"Unsupported item type in User message while formatting chat history for Azure AI"
f" Inference: {type(item)}"
)
return UserMessage(content=content_items)
def _format_assistant_message(message: ChatMessageContent) -> AssistantMessage:
"""Format an assistant message to the expected object for the client.
Args:
message: The assistant message.
Returns:
The formatted assistant message.
"""
tool_calls: list[ChatCompletionsToolCall] = []
for item in message.items:
if isinstance(item, TextContent):
# Assuming the assistant message will have only one text content item
# and we assign the content directly to the message content, which is a string.
continue
if isinstance(item, FunctionCallContent):
tool_calls.append(
ChatCompletionsToolCall(
id=item.id or "",
function=FunctionCall(
name=item.name or "",
arguments=json.dumps(item.arguments)
if isinstance(item.arguments, Mapping)
else item.arguments or "",
),
)
)
else:
logger.warning(
"Unsupported item type in Assistant message while formatting chat history for Azure AI"
f" Inference: {type(item)}"
)
# tollCalls cannot be an empty list, so we need to set it to None if it is empty
return AssistantMessage(content=message.content, tool_calls=tool_calls if tool_calls else None)
def _format_tool_message(message: ChatMessageContent) -> ToolMessage:
"""Format a tool message to the expected object for the client.
Args:
message: The tool message.
Returns:
The formatted tool message.
"""
if len(message.items) != 1:
logger.warning(
"Unsupported number of items in Tool message while formatting chat history for Azure AI"
f" Inference: {len(message.items)}"
)
if not isinstance(message.items[0], FunctionResultContent):
raise ValueError("No FunctionResultContent found in the message items")
# The API expects the result to be a string, so we need to convert it to a string
return ToolMessage(
content=str(message.items[0].result), tool_call_id=message.items[0].id if message.items[0].id else "None"
)
MESSAGE_CONVERTERS: dict[AuthorRole, Callable[[ChatMessageContent], ChatRequestMessage]] = {
AuthorRole.SYSTEM: _format_system_message,
AuthorRole.USER: _format_user_message,
AuthorRole.ASSISTANT: _format_assistant_message,
AuthorRole.TOOL: _format_tool_message,
AuthorRole.DEVELOPER: _format_developer_message,
}