chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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",
|
||||
]
|
||||
+100
@@ -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
|
||||
+41
@@ -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.
|
||||
+128
@@ -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())
|
||||
+395
@@ -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,
|
||||
}
|
||||
+108
@@ -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
|
||||
+54
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user