chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run

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 @@
# Copyright (c) Microsoft. All rights reserved.
@@ -0,0 +1,50 @@
# Copyright (c) Microsoft. All rights reserved.
import ast
import sys
from urllib.parse import quote_plus
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
class SearchLambdaVisitor(ast.NodeVisitor):
"""Visitor to parse a lambda function for Brave and Google Search filters."""
def __init__(self, valid_parameters: list[str]):
"""Initialize the visitor with a list of valid parameters."""
self.filters: list[dict[str, str]] = []
self.valid_parameters = valid_parameters
@override
def visit_Lambda(self, node):
self.visit(node.body)
@override
def visit_Compare(self, node):
# Only support x.FIELD == VALUE
if not (isinstance(node.left, ast.Attribute) and isinstance(node.left.value, ast.Name)):
raise NotImplementedError("Left side must be x.FIELD.")
field = node.left.attr
if not (len(node.ops) == 1 and isinstance(node.ops[0], ast.Eq)):
raise NotImplementedError("Only == comparisons are supported.")
right = node.comparators[0]
if isinstance(right, ast.Constant):
if right.value is None:
raise NotImplementedError("None values are not supported.")
value = str(right.value)
else:
raise NotImplementedError("Only constant values are supported on the right side.")
if field not in self.valid_parameters:
raise ValueError(f"Field '{field}' is not supported.")
self.filters.append({field: quote_plus(value)})
@override
def visit_BoolOp(self, node):
if not isinstance(node.op, ast.And):
raise NotImplementedError("Only 'and' of == comparisons is supported.")
for v in node.values:
self.visit(v)
@@ -0,0 +1,52 @@
# AI Connectors
This directory contains the implementation of the AI connectors (aka AI services) that are used to interact with AI models.
Depending on the modality, the AI connector can inherit from one of the following classes:
- [`ChatCompletionClientBase`](./chat_completion_client_base.py) for chat completion tasks.
- [`TextCompletionClientBase`](./text_completion_client_base.py) for text completion tasks.
- [`AudioToTextClientBase`](./audio_to_text_client_base.py) for audio to text tasks.
- [`TextToAudioClientBase`](./text_to_audio_client_base.py) for text to audio tasks.
- [`TextToImageClientBase`](./text_to_image_client_base.py) for text to image tasks.
- [`EmbeddingGeneratorBase`](./embeddings/embedding_generator_base.py) for text embedding tasks.
All base clients inherit from the [`AIServiceClientBase`](../../services/ai_service_client_base.py) class.
## Existing AI connectors
| Services | Connectors |
|-------------------------|--------------------------------------|
| OpenAI | [`OpenAIChatCompletion`](./open_ai/services/open_ai_chat_completion.py) |
| | [`OpenAITextCompletion`](./open_ai/services/open_ai_text_completion.py) |
| | [`OpenAITextEmbedding`](./open_ai/services/open_ai_text_embedding.py) |
| | [`OpenAITextToImage`](./open_ai/services/open_ai_text_to_image.py) |
| | [`OpenAITextToAudio`](./open_ai/services/open_ai_text_to_audio.py) |
| | [`OpenAIAudioToText`](./open_ai/services/open_ai_audio_to_text.py) |
| Azure OpenAI | [`AzureChatCompletion`](./open_ai/services/azure_chat_completion.py) |
| | [`AzureTextEmbedding`](./open_ai/services/azure_text_embedding.py) |
| | [`AzureTextToImage`](./open_ai/services/azure_text_to_image.py) |
| | [`AzureTextToAudio`](./open_ai/services/azure_text_to_audio.py) |
| | [`AzureAudioToText`](./open_ai/services/azure_audio_to_text.py) |
| Azure AI Inference | [`AzureAIInferenceChatCompletion`](./azure_ai_inference/services/azure_ai_inference_chat_completion.py) |
| | [`AzureAIInferenceTextEmbedding`](./azure_ai_inference/services/azure_ai_inference_text_embedding.py) |
| Anthropic | [`AnthropicChatCompletion`](./anthropic/services/anthropic_chat_completion.py) |
| [Bedrock](./bedrock/README.md) | [`BedrockChatCompletion`](./bedrock/services/bedrock_chat_completion.py) |
| | [`BedrockTextCompletion`](./bedrock/services/bedrock_text_completion.py) |
| | [`BedrockTextEmbedding`](./bedrock/services/bedrock_text_embedding.py) |
| [Google AI](./google/README.md) | [`GoogleAIChatCompletion`](./google/google_ai/services/google_ai_chat_completion.py) |
| | [`GoogleAITextCompletion`](./google/google_ai/services/google_ai_text_completion.py) |
| | [`GoogleAITextEmbedding`](./google/google_ai/services/google_ai_text_embedding.py) |
| [Vertex AI](./google/README.md) | [`GoogleAIChatCompletion`](./google/google_ai/services/google_ai_chat_completion.py) |
| | [`GoogleAITextCompletion`](./google/google_ai/services/google_ai_text_completion.py) |
| | [`GoogleAITextEmbedding`](./google/google_ai/services/google_ai_text_embedding.py) |
| HuggingFace | [`HuggingFaceTextCompletion`](./hugging_face/services/hf_text_completion.py) |
| | [`HuggingFaceTextEmbedding`](./hugging_face/services/hf_text_embedding.py) |
| Mistral AI | [`MistralAIChatCompletion`](./mistral_ai/services/mistral_ai_chat_completion.py) |
| | [`MistralAITextEmbedding`](./mistral_ai/services/mistral_ai_text_embedding.py) |
| [Nvidia](./nvidia/README.md) | [`NvidiaTextEmbedding`](./nvidia/services/nvidia_text_embedding.py) |
| Ollama | [`OllamaChatCompletion`](./ollama/services/ollama_chat_completion.py) |
| | [`OllamaTextCompletion`](./ollama/services/ollama_text_completion.py) |
| | [`OllamaTextEmbedding`](./ollama/services/ollama_text_embedding.py) |
| Onnx | [`OnnxGenAIChatCompletion`](./onnx/services/onnx_gen_ai_chat_completion.py) |
| | [`OnnxGenAITextCompletion`](./onnx/services/onnx_gen_ai_text_completion.py) |
@@ -0,0 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.completion_usage import CompletionUsage
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
__all__ = ["CompletionUsage", "FunctionChoiceBehavior", "PromptExecutionSettings"]
@@ -0,0 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.anthropic.prompt_execution_settings.anthropic_prompt_execution_settings import (
AnthropicChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.anthropic.services.anthropic_chat_completion import AnthropicChatCompletion
__all__ = [
"AnthropicChatCompletion",
"AnthropicChatPromptExecutionSettings",
]
@@ -0,0 +1,55 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
from typing import Annotated, Any
from pydantic import Field, model_validator
from semantic_kernel.connectors.ai.function_choice_type import FunctionChoiceType
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.exceptions import ServiceInvalidExecutionSettingsError
logger = logging.getLogger(__name__)
class AnthropicPromptExecutionSettings(PromptExecutionSettings):
"""Common request settings for Anthropic services."""
ai_model_id: Annotated[str | None, Field(serialization_alias="model")] = None
class AnthropicChatPromptExecutionSettings(AnthropicPromptExecutionSettings):
"""Specific settings for the Chat Completion endpoint."""
messages: list[dict[str, Any]] | None = None
stream: bool | None = None
system: str | None = None
max_tokens: Annotated[int, Field(gt=0)] = 1024
temperature: Annotated[float | None, Field(ge=0.0, le=2.0)] = None
stop_sequences: list[str] | None = None
top_p: Annotated[float | None, Field(ge=0.0, le=1.0)] = None
top_k: Annotated[int | None, Field(ge=0)] = None
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[
dict[str, 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="after")
def validate_tool_choice(self) -> "AnthropicChatPromptExecutionSettings":
"""Validate tool choice. Anthropic doesn't support NONE tool choice."""
tool_choice = self.tool_choice
if tool_choice and tool_choice.get("type") == FunctionChoiceType.NONE.value:
raise ServiceInvalidExecutionSettingsError("Tool choice 'none' is not supported by Anthropic.")
return self
@@ -0,0 +1,393 @@
# Copyright (c) Microsoft. All rights reserved.
import json
import logging
import sys
from collections.abc import AsyncGenerator, Callable
from typing import TYPE_CHECKING, Any, ClassVar
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
from anthropic import AsyncAnthropic
from anthropic.lib.streaming._types import TextEvent
from anthropic.types import (
ContentBlockStopEvent,
Message,
RawMessageDeltaEvent,
RawMessageStartEvent,
TextBlock,
ToolUseBlock,
)
from pydantic import ValidationError
from semantic_kernel.connectors.ai.anthropic.prompt_execution_settings.anthropic_prompt_execution_settings import (
AnthropicChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.anthropic.services.utils import (
MESSAGE_CONVERTERS,
update_settings_from_function_call_configuration,
)
from semantic_kernel.connectors.ai.anthropic.settings.anthropic_settings import AnthropicSettings
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
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 as SemanticKernelFinishReason
from semantic_kernel.exceptions.service_exceptions import (
ServiceInitializationError,
ServiceInvalidRequestError,
ServiceInvalidResponseError,
ServiceResponseException,
)
from semantic_kernel.utils.feature_stage_decorator import experimental
from semantic_kernel.utils.telemetry.model_diagnostics.decorators import (
trace_chat_completion,
trace_streaming_chat_completion,
)
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
# map finish reasons from Anthropic to Semantic Kernel
ANTHROPIC_TO_SEMANTIC_KERNEL_FINISH_REASON_MAP = {
"end_turn": SemanticKernelFinishReason.STOP,
"max_tokens": SemanticKernelFinishReason.LENGTH,
"tool_use": SemanticKernelFinishReason.TOOL_CALLS,
}
logger: logging.Logger = logging.getLogger(__name__)
@experimental
class AnthropicChatCompletion(ChatCompletionClientBase):
"""Anthropic ChatCompletion class."""
MODEL_PROVIDER_NAME: ClassVar[str] = "anthropic"
SUPPORTS_FUNCTION_CALLING: ClassVar[bool] = True
async_client: AsyncAnthropic
def __init__(
self,
ai_model_id: str | None = None,
service_id: str | None = None,
api_key: str | None = None,
async_client: AsyncAnthropic | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize an AnthropicChatCompletion service.
Args:
ai_model_id: Anthropic model name, see
https://docs.anthropic.com/en/docs/about-claude/models#model-names
service_id: Service ID tied to the execution settings.
api_key: The optional API key to use. If provided will override,
the env vars or .env file value.
async_client: An existing client to use.
env_file_path: Use the environment settings file as a fallback
to environment variables.
env_file_encoding: The encoding of the environment settings file.
"""
try:
anthropic_settings = AnthropicSettings(
api_key=api_key,
chat_model_id=ai_model_id,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as ex:
raise ServiceInitializationError("Failed to create Anthropic settings.", ex) from ex
if not anthropic_settings.chat_model_id:
raise ServiceInitializationError("The Anthropic chat model ID is required.")
if not async_client:
async_client = AsyncAnthropic(
api_key=anthropic_settings.api_key.get_secret_value(),
)
super().__init__(
async_client=async_client,
service_id=service_id or anthropic_settings.chat_model_id,
ai_model_id=anthropic_settings.chat_model_id,
)
# region Overriding base class methods
# Override from AIServiceClientBase
@override
def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]:
return AnthropicChatPromptExecutionSettings
# Override from AIServiceClientBase
@override
def service_url(self) -> str | None:
return str(self.async_client.base_url)
@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
@trace_chat_completion(MODEL_PROVIDER_NAME)
async def _inner_get_chat_message_contents(
self,
chat_history: "ChatHistory",
settings: "PromptExecutionSettings",
) -> list["ChatMessageContent"]:
if not isinstance(settings, AnthropicChatPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, AnthropicChatPromptExecutionSettings) # nosec
settings.ai_model_id = settings.ai_model_id or self.ai_model_id
settings.messages, parsed_system_message = self._prepare_chat_history_for_request(chat_history)
if settings.system is None and parsed_system_message is not None:
settings.system = parsed_system_message
return await self._send_chat_request(settings)
@override
@trace_streaming_chat_completion(MODEL_PROVIDER_NAME)
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, AnthropicChatPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, AnthropicChatPromptExecutionSettings) # nosec
settings.messages, parsed_system_message = self._prepare_chat_history_for_request(chat_history, stream=True)
settings.ai_model_id = settings.ai_model_id or self.ai_model_id
if settings.system is None and parsed_system_message is not None:
settings.system = parsed_system_message
response = self._send_chat_stream_request(settings, function_invoke_attempt)
if not isinstance(response, AsyncGenerator):
raise ServiceInvalidResponseError("Expected an AsyncGenerator response.")
async for message in response:
yield message
@override
def _prepare_chat_history_for_request(
self,
chat_history: "ChatHistory",
role_key: str = "role",
content_key: str = "content",
stream: bool = False,
) -> tuple[list[dict[str, Any]], str | None]:
"""Prepare the chat history for an Anthropic request.
Allowing customization of the key names for role/author, and optionally overriding the role.
Args:
chat_history: The chat history to prepare.
role_key: The key name for the role/author.
content_key: The key name for the content/message.
stream: Whether the request is for a streaming chat.
Returns:
A tuple containing the prepared chat history and the first SYSTEM message content.
"""
system_message_content = None
system_message_count = 0
formatted_messages: list[dict[str, Any]] = []
for i in range(len(chat_history)):
prev_message = chat_history[i - 1] if i > 0 else None
curr_message = chat_history[i]
if curr_message.role == AuthorRole.SYSTEM:
# Skip system messages after the first one is found
if system_message_count == 0:
system_message_content = curr_message.content
system_message_count += 1
elif curr_message.role == AuthorRole.USER or curr_message.role == AuthorRole.ASSISTANT:
formatted_messages.append(MESSAGE_CONVERTERS[curr_message.role](curr_message))
elif curr_message.role == AuthorRole.TOOL:
if prev_message is None:
# Under no circumstances should a tool message be the first message in the chat history
raise ServiceInvalidRequestError("Tool message found without a preceding message.")
if prev_message.role == AuthorRole.USER or prev_message.role == AuthorRole.SYSTEM:
# A tool message should not be found after a user or system message
# Please NOTE that in SK there are the USER role and the TOOL role, but in Anthropic
# the tool messages are considered as USER messages. We are checking against the SK roles.
raise ServiceInvalidRequestError("Tool message found after a user or system message.")
formatted_message = MESSAGE_CONVERTERS[curr_message.role](curr_message)
if prev_message.role == AuthorRole.ASSISTANT:
# The first tool message after an assistant message should be a new message
formatted_messages.append(formatted_message)
else:
# Append the tool message to the previous tool message.
# This indicates that the assistant message requested multiple parallel tool calls.
# Anthropic requires that parallel Tool messages are grouped together in a single message.
formatted_messages[-1][content_key] += formatted_message[content_key]
else:
raise ServiceInvalidRequestError(f"Unsupported role in chat history: {curr_message.role}")
if system_message_count > 1:
logger.warning(
"Anthropic service only supports one system message, but %s system messages were found."
" Only the first system message will be included in the request.",
system_message_count,
)
return formatted_messages, system_message_content
# endregion
def _create_chat_message_content(
self, response: Message, response_metadata: dict[str, Any]
) -> "ChatMessageContent":
"""Create a chat message content object."""
items: list[CMC_ITEM_TYPES] = []
items += self._get_tool_calls_from_message(response)
for content_block in response.content:
if isinstance(content_block, TextBlock):
items.append(TextContent(text=content_block.text))
finish_reason = None
if response.stop_reason:
finish_reason = ANTHROPIC_TO_SEMANTIC_KERNEL_FINISH_REASON_MAP[response.stop_reason]
return ChatMessageContent(
inner_content=response,
ai_model_id=self.ai_model_id,
metadata=response_metadata,
role=AuthorRole.ASSISTANT,
items=items,
finish_reason=finish_reason,
)
def _create_streaming_chat_message_content(
self,
stream_event: TextEvent | ContentBlockStopEvent | RawMessageDeltaEvent,
metadata: dict[str, Any] | None = None,
function_invoke_attempt: int = 0,
) -> StreamingChatMessageContent:
"""Create a streaming chat message content object from a content block."""
items: list[STREAMING_ITEM_TYPES] = []
finish_reason = None
if isinstance(stream_event, TextEvent):
items.append(StreamingTextContent(choice_index=0, text=stream_event.text))
elif (
isinstance(stream_event, ContentBlockStopEvent)
and hasattr(stream_event, "content_block")
and stream_event.content_block.type == "tool_use"
):
tool_use_block = stream_event.content_block
items.append(
FunctionCallContent(
id=tool_use_block.id,
index=stream_event.index,
name=tool_use_block.name,
arguments=json.dumps(tool_use_block.input) if tool_use_block.input else None,
)
)
elif isinstance(stream_event, RawMessageDeltaEvent):
finish_reason = ANTHROPIC_TO_SEMANTIC_KERNEL_FINISH_REASON_MAP[str(stream_event.delta.stop_reason)]
output_tokens = stream_event.usage.output_tokens
if metadata is None:
metadata = {"usage": {"output_tokens": output_tokens}}
else:
metadata = metadata | {"usage": metadata.get("usage", {}) | {"output_tokens": output_tokens}}
return StreamingChatMessageContent(
choice_index=0,
inner_content=stream_event,
ai_model_id=self.ai_model_id,
metadata=metadata,
role=AuthorRole.ASSISTANT,
finish_reason=finish_reason,
items=items,
function_invoke_attempt=function_invoke_attempt,
)
async def _send_chat_request(self, settings: AnthropicChatPromptExecutionSettings) -> list["ChatMessageContent"]:
"""Send the chat request."""
try:
response = await self.async_client.messages.create(**settings.prepare_settings_dict())
except Exception as ex:
raise ServiceResponseException(
f"{type(self)} service failed to complete the request",
ex,
) from ex
response_metadata: dict[str, Any] = {"id": response.id}
if hasattr(response, "usage") and response.usage is not None:
response_metadata["usage"] = response.usage
return [self._create_chat_message_content(response, response_metadata)]
async def _send_chat_stream_request(
self,
settings: AnthropicChatPromptExecutionSettings,
function_invoke_attempt: int = 0,
) -> AsyncGenerator[list["StreamingChatMessageContent"], None]:
"""Send the chat stream request.
The stream yields a sequence of stream events, which are used to create streaming chat message content:
- RawMessageStartEvent is used to determine the message id and input tokens.
- RawMessageDeltaEvent is used to determine the finish reason.
- TextEvent is used to determine the text content and ContentBlockStopEvent is used to determine
the tool use content.
"""
try:
async with self.async_client.messages.stream(**settings.prepare_settings_dict()) as stream:
metadata: dict[str, Any] = {"usage": {}, "id": None}
async for stream_event in stream:
if isinstance(stream_event, RawMessageStartEvent):
metadata["usage"]["input_tokens"] = stream_event.message.usage.input_tokens
metadata["id"] = stream_event.message.id
elif isinstance(stream_event, (TextEvent, RawMessageDeltaEvent)) or (
isinstance(stream_event, ContentBlockStopEvent)
and stream_event.content_block.type == "tool_use"
):
yield [
self._create_streaming_chat_message_content(stream_event, metadata, function_invoke_attempt)
]
except Exception as ex:
raise ServiceResponseException(
f"{type(self)} service failed to complete the request",
ex,
) from ex
def _get_tool_calls_from_message(self, message: Message) -> list[FunctionCallContent]:
"""Get tool calls from a content blocks."""
tool_calls: list[FunctionCallContent] = []
for idx, content_block in enumerate(message.content):
if isinstance(content_block, ToolUseBlock):
tool_calls.append(
FunctionCallContent(
id=content_block.id,
index=idx,
name=content_block.name,
arguments=getattr(content_block, "input", None),
)
)
return tool_calls
@@ -0,0 +1,157 @@
# Copyright (c) Microsoft. All rights reserved.
import json
import logging
from collections.abc import Callable, Mapping
from typing import TYPE_CHECKING, Any
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType
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.text_content import TextContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
logger: logging.Logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
def _format_user_message(message: ChatMessageContent) -> dict[str, Any]:
"""Format a user message to the expected object for the Anthropic client.
Args:
message: The user message.
Returns:
The formatted user message.
"""
return {
"role": "user",
"content": message.content,
}
def _format_assistant_message(message: ChatMessageContent) -> dict[str, Any]:
"""Format an assistant message to the expected object for the Anthropic client.
Args:
message: The assistant message.
Returns:
The formatted assistant message.
"""
tool_calls: list[dict[str, Any]] = []
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({
"type": "tool_use",
"id": item.id or "",
"name": item.name or "",
"input": item.arguments
if isinstance(item.arguments, Mapping)
else json.loads(item.arguments)
if item.arguments
else {},
})
else:
logger.warning(
f"Unsupported item type in Assistant message while formatting chat history for Anthropic: {type(item)}"
)
formatted_message: dict[str, Any] = {"role": "assistant", "content": []}
if message.content:
# Only include the text content if it is not empty.
# Otherwise, the Anthropic client will throw an error.
formatted_message["content"].append({ # type: ignore
"type": "text",
"text": message.content,
})
if tool_calls:
# Only include the tool calls if there are any.
# Otherwise, the Anthropic client will throw an error.
formatted_message["content"].extend(tool_calls) # type: ignore
return formatted_message
def _format_tool_message(message: ChatMessageContent) -> dict[str, Any]:
"""Format a tool message to the expected object for the Anthropic client.
Args:
message: The tool message.
Returns:
The formatted tool message.
"""
function_result_contents: list[dict[str, Any]] = []
for item in message.items:
if not isinstance(item, FunctionResultContent):
logger.warning(
f"Unsupported item type in Tool message while formatting chat history for Anthropic: {type(item)}"
)
continue
function_result_contents.append({
"type": "tool_result",
"tool_use_id": item.id,
"content": str(item.result),
})
return {
"role": "user",
"content": function_result_contents,
}
MESSAGE_CONVERTERS: dict[AuthorRole, Callable[[ChatMessageContent], dict[str, Any]]] = {
AuthorRole.USER: _format_user_message,
AuthorRole.ASSISTANT: _format_assistant_message,
AuthorRole.TOOL: _format_tool_message,
}
def update_settings_from_function_call_configuration(
function_choice_configuration: "FunctionCallChoiceConfiguration",
settings: "PromptExecutionSettings",
type: FunctionChoiceType,
) -> None:
"""Update the settings from a FunctionChoiceConfiguration."""
if (
function_choice_configuration.available_functions
and hasattr(settings, "tools")
and hasattr(settings, "tool_choice")
):
settings.tools = [
kernel_function_metadata_to_function_call_format(f)
for f in function_choice_configuration.available_functions
]
if (
settings.function_choice_behavior and settings.function_choice_behavior.type_ == FunctionChoiceType.REQUIRED
) or type == FunctionChoiceType.REQUIRED:
settings.tool_choice = {"type": "any"}
else:
settings.tool_choice = {"type": type.value}
def kernel_function_metadata_to_function_call_format(metadata: KernelFunctionMetadata) -> dict[str, Any]:
"""Convert the kernel function metadata to function calling format."""
return {
"name": metadata.fully_qualified_name,
"description": metadata.description or "",
"input_schema": {
"type": "object",
"properties": {p.name: p.schema_data for p in metadata.parameters},
"required": [p.name for p in metadata.parameters if p.is_required],
},
}
@@ -0,0 +1,29 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import ClassVar
from pydantic import SecretStr
from semantic_kernel.kernel_pydantic import KernelBaseSettings
class AnthropicSettings(KernelBaseSettings):
"""Anthropic model settings.
The settings are first loaded from environment variables with the prefix 'ANTHROPIC_'. 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.
Optional settings for prefix 'ANTHROPIC_' are:
- api_key: ANTHROPIC API key, see https://console.anthropic.com/settings/keys
(Env var ANTHROPIC_API_KEY)
- chat_model_id: The Anthropic chat model ID to use see https://docs.anthropic.com/en/docs/about-claude/models.
(Env var ANTHROPIC_CHAT_MODEL_ID)
- env_file_path: if provided, the .env settings are read from this file path location
"""
env_prefix: ClassVar[str] = "ANTHROPIC_"
api_key: SecretStr
chat_model_id: str | None = None
@@ -0,0 +1,51 @@
# Copyright (c) Microsoft. All rights reserved.
from abc import ABC, abstractmethod
from typing import Any
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.contents.audio_content import AudioContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.services.ai_service_client_base import AIServiceClientBase
class AudioToTextClientBase(AIServiceClientBase, ABC):
"""Base class for audio to text client."""
@abstractmethod
async def get_text_contents(
self,
audio_content: AudioContent,
settings: PromptExecutionSettings | None = None,
**kwargs: Any,
) -> list[TextContent]:
"""Get text contents from audio.
Args:
audio_content: Audio content.
settings: Prompt execution settings.
kwargs: Additional arguments.
Returns:
list[TextContent]: Text contents.
"""
raise NotImplementedError
async def get_text_content(
self,
audio_content: AudioContent,
settings: PromptExecutionSettings | None = None,
**kwargs: Any,
) -> TextContent:
"""Get text content from audio.
Args:
audio_content: Audio content.
settings: Prompt execution settings.
kwargs: Additional arguments.
Returns:
TextContent: Text content.
"""
return (await self.get_text_contents(audio_content, settings, **kwargs))[0]
@@ -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,
}
@@ -0,0 +1,75 @@
# Amazon - Bedrock
[Amazon Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html) is a service provided by Amazon Web Services (AWS) that allows you to access large language models with a serverless experience. Semantic Kernel provides a connector to access these models from AWS.
## Prerequisites
- An AWS account and [access to the foundation models](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access-permissions.html)
- [AWS CLI installed](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) and [configured](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html#configuration)
### Configuration
Follow this [guide](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html#configuration) to configure your environment to use the Bedrock API.
Please configure the `aws_access_key_id`, `aws_secret_access_key`, and `region` otherwise you will need to create custom clients for the services. For example:
```python
runtime_client=boto.client(
"bedrock-runtime",
aws_access_key_id="your_access_key",
aws_secret_access_key="your_secret_key",
region_name="your_region",
[...other parameters you may need...]
)
client=boto.client(
"bedrock",
aws_access_key_id="your_access_key",
aws_secret_access_key="your_secret_key",
region_name="your_region",
[...other parameters you may need...]
)
bedrock_chat_completion_service = BedrockChatCompletion(runtime_client=runtime_client, client=client)
```
## Supports
### Region
To find model supports by AWS regions, refer to this [AWS documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/models-regions.html).
### Inference profiles
You can create inference profiles in AWS Bedrock to monitor and optimize the performance of your foundation models. Refer to the [AWS documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles.html) for more information.
When you are using an Application Inference Profile, you must specify the `BEDROCK_MODEL_PROVIDER` environment variable to the model provider you are using. For example, if you are using Amazon Titan, you must set `BEDROCK_MODEL_PROVIDER=amazon`. This is because an Application Inference Profile doesn't contain the model provider information, and the Bedrock connector needs to know which model provider to use so that it can create the correct request body to the Bedrock API.
> An Application Inference Profile ARN is usually formatted as followed: `arn:aws:bedrock:<region>:<account-id>:application-inference-profile/<profile-id>`.
### Input & Output Modalities
Foundational models in Bedrock support the multiple modalities, including text, image, and embedding. However, not all models support the same modalities. Refer to the [AWS documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html) for more information.
The Bedrock connector supports all modalities except for **image embeddings, and text to image**.
### Text completion vs chat completion
Some models in Bedrock supports only text completion, or only chat completion (aka Converse API), or both. Refer to the [AWS documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/models-features.html) for more information.
### Tool Use
Not all models in Bedrock support tools. Refer to the [AWS documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/models-features.html) for more information.
### Streaming vs Non-Streaming
Not all models in Bedrock support streaming. You can use the boto3 client to check if a model supports streaming. Refer to the [AWS documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-supported-models-features.html) and the [Boto3 documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock/client/get_foundation_model.html) for more information.
## Model specific parameters
Foundation models can have specific parameters that are unique to the model or the model provider. You can refer to this [AWS documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters.html) for more information.
You can pass these parameters via the `extension_data` field in the `PromptExecutionSettings` object.
## Unsupported features
- [Guardrail](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html)
@@ -0,0 +1,23 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.bedrock.bedrock_prompt_execution_settings import (
BedrockChatPromptExecutionSettings,
BedrockEmbeddingPromptExecutionSettings,
BedrockPromptExecutionSettings,
BedrockTextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.bedrock.bedrock_settings import BedrockSettings
from semantic_kernel.connectors.ai.bedrock.services.bedrock_chat_completion import BedrockChatCompletion
from semantic_kernel.connectors.ai.bedrock.services.bedrock_text_completion import BedrockTextCompletion
from semantic_kernel.connectors.ai.bedrock.services.bedrock_text_embedding import BedrockTextEmbedding
__all__ = [
"BedrockChatCompletion",
"BedrockChatPromptExecutionSettings",
"BedrockEmbeddingPromptExecutionSettings",
"BedrockPromptExecutionSettings",
"BedrockSettings",
"BedrockTextCompletion",
"BedrockTextEmbedding",
"BedrockTextPromptExecutionSettings",
]
@@ -0,0 +1,49 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Annotated, Any
from pydantic import Field
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
class BedrockPromptExecutionSettings(PromptExecutionSettings):
"""Bedrock Prompt Execution Settings."""
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
top_k: Annotated[int | None, Field(gt=0)] = None
max_tokens: Annotated[int | None, Field(gt=0)] = None
stop: list[str] = Field(default_factory=list)
class BedrockChatPromptExecutionSettings(BedrockPromptExecutionSettings):
"""Bedrock Chat Prompt Execution Settings."""
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[
dict[str, Any] | None,
Field(
description="Do not set this manually. It is set by the service based "
"on the function choice configuration.",
),
] = None
class BedrockTextPromptExecutionSettings(BedrockPromptExecutionSettings):
"""Bedrock Text Prompt Execution Settings."""
...
class BedrockEmbeddingPromptExecutionSettings(PromptExecutionSettings):
"""Bedrock Embedding Prompt Execution Settings."""
...
@@ -0,0 +1,41 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import ClassVar
from semantic_kernel.connectors.ai.bedrock.services.model_provider.bedrock_model_provider import BedrockModelProvider
from semantic_kernel.kernel_pydantic import KernelBaseSettings
from semantic_kernel.utils.feature_stage_decorator import experimental
@experimental
class BedrockSettings(KernelBaseSettings):
"""Amazon Bedrock service settings.
The settings are first loaded from environment variables with
the prefix 'BEDROCK_'.
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.
Optional settings for prefix 'BEDROCK_' are:
- chat_model_id: str | None - The Amazon Bedrock chat model ID to use.
(Env var BEDROCK_CHAT_MODEL_ID)
- text_model_id: str | None - The Amazon Bedrock text model ID to use.
(Env var BEDROCK_TEXT_MODEL_ID)
- embedding_model_id: str | None - The Amazon Bedrock embedding model ID to use.
(Env var BEDROCK_EMBEDDING_MODEL_ID)
- model_provider: BedrockModelProvider | None - The Bedrock model provider to use.
If not provided, the model provider will be extracted from the model ID.
When using an Application Inference Profile where the model provider is not part
of the model ID, this setting must be provided.
(Env var BEDROCK_MODEL_PROVIDER)
"""
env_prefix: ClassVar[str] = "BEDROCK_"
chat_model_id: str | None = None
text_model_id: str | None = None
embedding_model_id: str | None = None
model_provider: BedrockModelProvider | None = None
@@ -0,0 +1,52 @@
# Copyright (c) Microsoft. All rights reserved.
from abc import ABC
from typing import Any, ClassVar
import boto3
from botocore.config import Config
from semantic_kernel.connectors.ai.bedrock.services.model_provider.bedrock_model_provider import BedrockModelProvider
from semantic_kernel.kernel_pydantic import KernelBaseModel
class BedrockBase(KernelBaseModel, ABC):
"""Amazon Bedrock Service Base Class."""
MODEL_PROVIDER_NAME: ClassVar[str] = "bedrock"
# Amazon Bedrock Clients
# Runtime Client: Use for inference
bedrock_runtime_client: Any
# Client: Use for model management
bedrock_client: Any
bedrock_model_provider: BedrockModelProvider | None = None
def __init__(
self,
*,
runtime_client: Any | None = None,
client: Any | None = None,
bedrock_model_provider: BedrockModelProvider | None = None,
**kwargs: Any,
) -> None:
"""Initialize the Amazon Bedrock Base Class.
Args:
runtime_client: The Amazon Bedrock runtime client to use.
client: The Amazon Bedrock client to use.
bedrock_model_provider: The Bedrock model provider to use.
If not provided, the model provider will be extracted from the model ID.
When using an Application Inference Profile where the model provider is not part
of the model ID, this setting must be provided.
**kwargs: Additional keyword arguments.
"""
config = Config(user_agent_extra="x-client-framework:semantic-kernel")
super().__init__(
bedrock_runtime_client=runtime_client or boto3.client("bedrock-runtime", config=config),
bedrock_client=client or boto3.client("bedrock"),
bedrock_model_provider=bedrock_model_provider,
**kwargs,
)
@@ -0,0 +1,401 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from collections.abc import AsyncGenerator, Callable
from functools import partial
from typing import TYPE_CHECKING, Any, ClassVar
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
from pydantic import ValidationError
from semantic_kernel.connectors.ai.bedrock.bedrock_prompt_execution_settings import BedrockChatPromptExecutionSettings
from semantic_kernel.connectors.ai.bedrock.bedrock_settings import BedrockSettings
from semantic_kernel.connectors.ai.bedrock.services.bedrock_base import BedrockBase
from semantic_kernel.connectors.ai.bedrock.services.model_provider.bedrock_model_provider import (
BedrockModelProvider,
get_chat_completion_additional_model_request_fields,
)
from semantic_kernel.connectors.ai.bedrock.services.model_provider.utils import (
MESSAGE_CONVERTERS,
finish_reason_from_bedrock_to_semantic_kernel,
remove_none_recursively,
update_settings_from_function_choice_configuration,
)
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_choice_behavior import FunctionChoiceType
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.image_content import ImageContent
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 ServiceInitializationError, ServiceInvalidResponseError
from semantic_kernel.utils.async_utils import run_in_executor
from semantic_kernel.utils.telemetry.model_diagnostics.decorators import (
trace_chat_completion,
trace_streaming_chat_completion,
)
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.contents.chat_history import ChatHistory
class BedrockChatCompletion(BedrockBase, ChatCompletionClientBase):
"""Amazon Bedrock Chat Completion Service."""
SUPPORTS_FUNCTION_CALLING: ClassVar[bool] = True
def __init__(
self,
model_id: str | None = None,
model_provider: BedrockModelProvider | None = None,
service_id: str | None = None,
runtime_client: Any | None = None,
client: Any | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize the Amazon Bedrock Chat Completion Service.
Args:
model_id: The Amazon Bedrock chat model ID to use.
model_provider: The Bedrock model provider to use.
service_id: The Service ID for the completion service.
runtime_client: The Amazon Bedrock runtime client to use.
client: The Amazon Bedrock client to use.
env_file_path: The path to the .env file.
env_file_encoding: The encoding of the .env file.
"""
try:
bedrock_settings = BedrockSettings(
chat_model_id=model_id,
model_provider=model_provider,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as e:
raise ServiceInitializationError("Failed to initialize the Amazon Bedrock Chat Completion Service.") from e
if bedrock_settings.chat_model_id is None:
raise ServiceInitializationError("The Amazon Bedrock Chat Model ID is missing.")
super().__init__(
ai_model_id=bedrock_settings.chat_model_id,
service_id=service_id or bedrock_settings.chat_model_id,
runtime_client=runtime_client,
client=client,
bedrock_model_provider=bedrock_settings.model_provider,
)
# region Overriding base class methods
# Override from AIServiceClientBase
@override
def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]:
return BedrockChatPromptExecutionSettings
@override
@trace_chat_completion(BedrockBase.MODEL_PROVIDER_NAME)
async def _inner_get_chat_message_contents(
self,
chat_history: "ChatHistory",
settings: "PromptExecutionSettings",
) -> list["ChatMessageContent"]:
if not isinstance(settings, BedrockChatPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, BedrockChatPromptExecutionSettings) # nosec
prepared_settings = self._prepare_settings_for_request(chat_history, settings)
response = await self._async_converse(**prepared_settings)
return [self._create_chat_message_content(response)]
@override
@trace_streaming_chat_completion(BedrockBase.MODEL_PROVIDER_NAME)
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, BedrockChatPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, BedrockChatPromptExecutionSettings) # nosec
prepared_settings = self._prepare_settings_for_request(chat_history, settings)
response_stream = await self._async_converse_streaming(**prepared_settings)
for event in response_stream.get("stream"):
if "messageStart" in event:
yield [self._parse_message_start_event(event)]
elif "contentBlockStart" in event:
yield [self._parse_content_block_start_event(event)]
elif "contentBlockDelta" in event:
yield [self._parse_content_block_delta_event(event, function_invoke_attempt)]
elif "contentBlockStop" in event:
continue
elif "messageStop" in event:
yield [self._parse_message_stop_event(event)]
elif "metadata" in event:
yield [self._parse_metadata_event(event)]
else:
raise ServiceInvalidResponseError(f"Unknown event type in the response: {event}")
@override
def _update_function_choice_settings_callback(
self,
) -> Callable[["FunctionCallChoiceConfiguration", "PromptExecutionSettings", FunctionChoiceType], None]:
return update_settings_from_function_choice_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",
) -> Any:
messages: list[dict[str, Any]] = []
for message in chat_history.messages:
if message.role == AuthorRole.SYSTEM:
continue
messages.append(MESSAGE_CONVERTERS[message.role](message))
return messages
# endregion
def _prepare_system_messages_for_request(self, chat_history: "ChatHistory") -> Any:
messages: list[dict[str, Any]] = []
for message in chat_history.messages:
if message.role != AuthorRole.SYSTEM:
continue
messages.append(MESSAGE_CONVERTERS[message.role](message))
return messages
def _prepare_settings_for_request(
self,
chat_history: "ChatHistory",
settings: "BedrockChatPromptExecutionSettings",
) -> dict[str, Any]:
"""Prepare the settings for the request.
Settings are prepared based on the syntax shown here:
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock-runtime/client/converse.html
Note that Guardrails are not supported.
"""
prepared_settings = {
"modelId": self.ai_model_id,
"messages": self._prepare_chat_history_for_request(chat_history),
"system": self._prepare_system_messages_for_request(chat_history),
"inferenceConfig": remove_none_recursively({
"maxTokens": settings.max_tokens,
"temperature": settings.temperature,
"topP": settings.top_p,
"stopSequences": settings.stop,
}),
"additionalModelRequestFields": get_chat_completion_additional_model_request_fields(
self.ai_model_id, settings, model_provider=self.bedrock_model_provider
),
}
if settings.tools and settings.tool_choice:
prepared_settings["toolConfig"] = {
"tools": settings.tools,
"toolChoice": settings.tool_choice,
}
return prepared_settings
def _create_chat_message_content(self, response: dict[str, Any]) -> ChatMessageContent:
"""Create a chat message content object."""
finish_reason: FinishReason | None = finish_reason_from_bedrock_to_semantic_kernel(response["stopReason"])
usage = CompletionUsage(
prompt_tokens=response["usage"]["inputTokens"],
completion_tokens=response["usage"]["outputTokens"],
)
items: list[CMC_ITEM_TYPES] = []
for content in response["output"]["message"]["content"]:
if "text" in content:
items.append(TextContent(text=content["text"], inner_content=content))
elif "image" in content:
items.append(
ImageContent(
data=content["image"]["source"]["bytes"],
mime_type=content["image"]["source"]["format"],
inner_content=content["image"],
)
)
elif "toolUse" in content:
items.append(
FunctionCallContent(
id=content["toolUse"]["toolUseId"],
name=content["toolUse"]["name"],
arguments=content["toolUse"]["input"],
)
)
else:
raise ServiceInvalidResponseError(f"Unsupported content type in the response: {content}")
return ChatMessageContent(
ai_model_id=self.ai_model_id,
role=AuthorRole.ASSISTANT,
items=items,
inner_content=response,
finish_reason=finish_reason,
metadata={"usage": usage},
)
# region async helper methods
async def _async_converse(self, **kwargs) -> Any:
"""Invoke the model asynchronously."""
return await run_in_executor(
None,
partial(
self.bedrock_runtime_client.converse,
**kwargs,
),
)
async def _async_converse_streaming(self, **kwargs) -> Any:
"""Invoke the model asynchronously."""
return await run_in_executor(
None,
partial(
self.bedrock_runtime_client.converse_stream,
**kwargs,
),
)
# endregion
# region streaming event parsing methods
def _parse_message_start_event(self, event: dict[str, Any]) -> StreamingChatMessageContent:
"""Parse the message start event.
The message start event contains the role of the message.
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_MessageStartEvent.html
"""
return StreamingChatMessageContent(
ai_model_id=self.ai_model_id,
role=AuthorRole(event["messageStart"]["role"]),
items=[],
choice_index=0,
inner_content=event,
)
def _parse_content_block_start_event(self, event: dict[str, Any]) -> StreamingChatMessageContent:
"""Parse the content block start event.
The content block start event contains tool information.
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ContentBlockStartEvent.html
"""
items: list[STREAMING_ITEM_TYPES] = []
if "toolUse" in event["contentBlockStart"]["start"]:
items.append(
FunctionCallContent(
id=event["contentBlockStart"]["start"]["toolUse"]["toolUseId"],
name=event["contentBlockStart"]["start"]["toolUse"]["name"],
index=event["contentBlockStart"]["contentBlockIndex"],
)
)
return StreamingChatMessageContent(
ai_model_id=self.ai_model_id,
role=AuthorRole.ASSISTANT, # Assume the role is always assistant
items=items,
choice_index=0,
inner_content=event,
)
def _parse_content_block_delta_event(
self, event: dict[str, Any], function_invoke_attempt: int
) -> StreamingChatMessageContent:
"""Parse the content block delta event.
The content block delta event contains the completion.
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ContentBlockDeltaEvent.html
"""
items: list[STREAMING_ITEM_TYPES] = [
StreamingTextContent(
choice_index=0,
text=event["contentBlockDelta"]["delta"]["text"],
inner_content=event,
)
if "text" in event["contentBlockDelta"]["delta"]
else FunctionCallContent(
arguments=event["contentBlockDelta"]["delta"]["toolUse"]["input"],
inner_content=event,
index=event["contentBlockDelta"]["contentBlockIndex"],
)
]
return StreamingChatMessageContent(
ai_model_id=self.ai_model_id,
role=AuthorRole.ASSISTANT, # Assume the role is always assistant
items=items,
choice_index=0,
inner_content=event,
function_invoke_attempt=function_invoke_attempt,
)
def _parse_message_stop_event(self, event: dict[str, Any]) -> StreamingChatMessageContent:
"""Parse the message stop event.
The message stop event contains the finish reason.
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_MessageStopEvent.html
"""
metadata = event["messageStop"].get("additionalModelResponseFields", {})
return StreamingChatMessageContent(
ai_model_id=self.ai_model_id,
role=AuthorRole.ASSISTANT, # Assume the role is always assistant
items=[],
choice_index=0,
inner_content=event,
finish_reason=finish_reason_from_bedrock_to_semantic_kernel(event["messageStop"]["stopReason"]),
metadata=metadata,
)
def _parse_metadata_event(self, event: dict[str, Any]) -> StreamingChatMessageContent:
"""Parse the metadata event.
The metadata event contains additional information.
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ConverseStreamMetadataEvent.html
"""
usage = CompletionUsage(
prompt_tokens=event["metadata"]["usage"]["inputTokens"],
completion_tokens=event["metadata"]["usage"]["outputTokens"],
)
return StreamingChatMessageContent(
ai_model_id=self.ai_model_id,
role=AuthorRole.ASSISTANT, # Assume the role is always assistant
items=[],
choice_index=0,
inner_content=event,
metadata={"usage": usage},
)
# endregion
@@ -0,0 +1,169 @@
# Copyright (c) Microsoft. All rights reserved.
import json
import sys
from collections.abc import AsyncGenerator
from functools import partial
from typing import TYPE_CHECKING, Any
from pydantic import ValidationError
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
from semantic_kernel.connectors.ai.bedrock.bedrock_prompt_execution_settings import BedrockTextPromptExecutionSettings
from semantic_kernel.connectors.ai.bedrock.bedrock_settings import BedrockSettings
from semantic_kernel.connectors.ai.bedrock.services.bedrock_base import BedrockBase
from semantic_kernel.connectors.ai.bedrock.services.model_provider.bedrock_model_provider import (
BedrockModelProvider,
get_text_completion_request_body,
parse_streaming_text_completion_response,
parse_text_completion_response,
)
from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
from semantic_kernel.utils.async_utils import run_in_executor
from semantic_kernel.utils.telemetry.model_diagnostics.decorators import (
trace_streaming_text_completion,
trace_text_completion,
)
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
class BedrockTextCompletion(BedrockBase, TextCompletionClientBase):
"""Amazon Bedrock Text Completion Service."""
def __init__(
self,
model_id: str | None = None,
model_provider: BedrockModelProvider | None = None,
service_id: str | None = None,
runtime_client: Any | None = None,
client: Any | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize the Amazon Bedrock Text Completion Service.
Args:
model_id: The Amazon Bedrock text model ID to use.
model_provider: The Bedrock model provider to use.
service_id: The Service ID for the text completion service.
runtime_client: The Amazon Bedrock runtime client to use.
client: The Amazon Bedrock client to use.
env_file_path: The path to the .env file to load settings from.
env_file_encoding: The encoding of the .env file.
"""
try:
bedrock_settings = BedrockSettings(
text_model_id=model_id,
model_provider=model_provider,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as e:
raise ServiceInitializationError("Failed to initialize the Amazon Bedrock Text Completion Service.") from e
if bedrock_settings.text_model_id is None:
raise ServiceInitializationError("The Amazon Bedrock Text Model ID is missing.")
super().__init__(
ai_model_id=bedrock_settings.text_model_id,
service_id=service_id or bedrock_settings.text_model_id,
runtime_client=runtime_client,
client=client,
bedrock_model_provider=bedrock_settings.model_provider,
)
# region Overriding base class methods
# Override from AIServiceClientBase
@override
def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]:
return BedrockTextPromptExecutionSettings
@override
@trace_text_completion(BedrockBase.MODEL_PROVIDER_NAME)
async def _inner_get_text_contents(
self,
prompt: str,
settings: "PromptExecutionSettings",
) -> list[TextContent]:
if not isinstance(settings, BedrockTextPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, BedrockTextPromptExecutionSettings) # nosec
request_body = get_text_completion_request_body(
self.ai_model_id,
prompt,
settings,
model_provider=self.bedrock_model_provider,
)
response_body = await self._async_invoke_model(request_body)
return parse_text_completion_response(
self.ai_model_id,
json.loads(response_body.get("body").read()),
model_provider=self.bedrock_model_provider,
)
@override
@trace_streaming_text_completion(BedrockBase.MODEL_PROVIDER_NAME)
async def _inner_get_streaming_text_contents(
self,
prompt: str,
settings: "PromptExecutionSettings",
) -> AsyncGenerator[list[StreamingTextContent], Any]:
if not isinstance(settings, BedrockTextPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, BedrockTextPromptExecutionSettings) # nosec
request_body = get_text_completion_request_body(
self.ai_model_id,
prompt,
settings,
model_provider=self.bedrock_model_provider,
)
response_stream = await self._async_invoke_model_stream(request_body)
for event in response_stream.get("body"):
chunk = event.get("chunk")
yield [
parse_streaming_text_completion_response(
self.ai_model_id,
json.loads(chunk.get("bytes").decode()),
model_provider=self.bedrock_model_provider,
)
]
# endregion
async def _async_invoke_model(self, request_body: dict) -> Any:
"""Invoke the model asynchronously."""
return await run_in_executor(
None,
partial(
self.bedrock_runtime_client.invoke_model,
body=json.dumps(request_body),
modelId=self.ai_model_id,
accept="application/json",
contentType="application/json",
),
)
async def _async_invoke_model_stream(self, request_body: dict) -> Any:
"""Invoke the model asynchronously and return a response stream."""
return await run_in_executor(
None,
partial(
self.bedrock_runtime_client.invoke_model_with_response_stream,
body=json.dumps(request_body),
modelId=self.ai_model_id,
accept="application/json",
contentType="application/json",
),
)
@@ -0,0 +1,135 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import json
import sys
from functools import partial
from typing import TYPE_CHECKING, Any
from numpy import array, ndarray
from pydantic import ValidationError
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
from semantic_kernel.connectors.ai.bedrock.bedrock_prompt_execution_settings import (
BedrockEmbeddingPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.bedrock.bedrock_settings import BedrockSettings
from semantic_kernel.connectors.ai.bedrock.services.bedrock_base import BedrockBase
from semantic_kernel.connectors.ai.bedrock.services.model_provider.bedrock_model_provider import (
BedrockModelProvider,
get_text_embedding_request_body,
parse_text_embedding_response,
)
from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
from semantic_kernel.utils.async_utils import run_in_executor
if TYPE_CHECKING:
pass
class BedrockTextEmbedding(BedrockBase, EmbeddingGeneratorBase):
"""Amazon Bedrock Text Embedding Service."""
def __init__(
self,
model_id: str | None = None,
model_provider: BedrockModelProvider | None = None,
service_id: str | None = None,
runtime_client: Any | None = None,
client: Any | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize the Amazon Bedrock Text Embedding Service.
Args:
model_id: The Amazon Bedrock text embedding model ID to use.
model_provider: The Bedrock model provider to use.
service_id: The Service ID for the text embedding service.
runtime_client: The Amazon Bedrock runtime client to use.
client: The Amazon Bedrock client to use.
env_file_path: The path to the .env file to load settings from.
env_file_encoding: The encoding of the .env file.
"""
try:
bedrock_settings = BedrockSettings(
embedding_model_id=model_id,
model_provider=model_provider,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as e:
raise ServiceInitializationError("Failed to initialize the Amazon Bedrock Text Embedding Service.") from e
if bedrock_settings.embedding_model_id is None:
raise ServiceInitializationError("The Amazon Bedrock Text Embedding Model ID is missing.")
super().__init__(
ai_model_id=bedrock_settings.embedding_model_id,
service_id=service_id or bedrock_settings.embedding_model_id,
runtime_client=runtime_client,
client=client,
bedrock_model_provider=bedrock_settings.model_provider,
)
@override
async def generate_embeddings(
self,
texts: list[str],
settings: "PromptExecutionSettings | None" = None,
**kwargs: Any,
) -> ndarray:
if not settings:
settings = BedrockEmbeddingPromptExecutionSettings()
elif not isinstance(settings, BedrockEmbeddingPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, BedrockEmbeddingPromptExecutionSettings) # nosec
results = await asyncio.gather(*[
self._async_invoke_model(
get_text_embedding_request_body(
self.ai_model_id,
text,
settings,
model_provider=self.bedrock_model_provider,
)
)
for text in texts
])
return array([
array(
parse_text_embedding_response(
self.ai_model_id,
json.loads(result.get("body").read()),
model_provider=self.bedrock_model_provider,
)
)
for result in results
])
@override
def get_prompt_execution_settings_class(
self,
) -> type["PromptExecutionSettings"]:
"""Get the request settings class."""
return BedrockEmbeddingPromptExecutionSettings
async def _async_invoke_model(self, request_body: dict) -> Any:
"""Invoke the model asynchronously."""
return await run_in_executor(
None,
partial(
self.bedrock_runtime_client.invoke_model,
body=json.dumps(request_body),
modelId=self.ai_model_id,
accept="application/json",
contentType="application/json",
),
)
@@ -0,0 +1,70 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
from semantic_kernel.connectors.ai.bedrock.bedrock_prompt_execution_settings import (
BedrockChatPromptExecutionSettings,
BedrockTextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.bedrock.services.model_provider.utils import remove_none_recursively
from semantic_kernel.contents.text_content import TextContent
# region Text Completion
def get_text_completion_request_body(prompt: str, settings: BedrockTextPromptExecutionSettings) -> Any:
"""Get the request body for text completion for AI21 Labs models.
https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-jurassic2.html
"""
return remove_none_recursively({
"prompt": prompt,
"temperature": settings.temperature,
"topP": settings.top_p,
"maxTokens": settings.max_tokens,
"stopSequences": settings.stop,
# Extension data
"countPenalty": settings.extension_data.get("countPenalty", None),
"presencePenalty": settings.extension_data.get("presencePenalty", None),
"frequencyPenalty": settings.extension_data.get("frequencyPenalty", None),
})
def parse_text_completion_response(response: dict[str, Any], model_id: str) -> list[TextContent]:
"""Parse the response from text completion for AI21 Labs models."""
return [
TextContent(
ai_model_id=model_id,
text=completion["data"]["text"],
inner_content=completion,
)
for completion in response.get("completions", [])
]
# endregion
# region Chat Completion
def get_chat_completion_additional_model_request_fields(
settings: BedrockChatPromptExecutionSettings,
) -> dict[str, Any] | None:
"""Get the additional model request fields for chat completion for AI21 Labs models.
https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-jamba.html
Note: We are not supporting multiple responses for now.
"""
additional_fields: dict[str, Any] = remove_none_recursively({
"frequency_penalty": settings.extension_data.get("frequency_penalty", None),
"presence_penalty": settings.extension_data.get("presence_penalty", None),
})
if not additional_fields:
return None
return additional_fields
# endregion
@@ -0,0 +1,113 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
from semantic_kernel.connectors.ai.bedrock.bedrock_prompt_execution_settings import (
BedrockChatPromptExecutionSettings,
BedrockEmbeddingPromptExecutionSettings,
BedrockTextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.bedrock.services.model_provider.utils import remove_none_recursively
from semantic_kernel.connectors.ai.completion_usage import CompletionUsage
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.exceptions.service_exceptions import ServiceInvalidResponseError
# region Text Completion
def get_text_completion_request_body(prompt: str, settings: BedrockTextPromptExecutionSettings) -> Any:
"""Get the request body for text completion for Amazon Titan models.
https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-titan-text.html
"""
return remove_none_recursively({
"inputText": prompt,
"textGenerationConfig": {
"temperature": settings.temperature,
"topP": settings.top_p,
"maxTokenCount": settings.max_tokens,
"stopSequences": settings.stop,
},
})
def parse_text_completion_response(response: dict[str, Any], model_id: str) -> list[TextContent]:
"""Parse the response from text completion for Amazon Titan models."""
prompt_tokens = response.get("inputTextTokenCount")
return [
TextContent(
ai_model_id=model_id,
text=completion["outputText"],
inner_content=completion,
metadata={
"usage": CompletionUsage(
prompt_tokens=prompt_tokens,
completion_tokens=response.get("tokenCount"),
)
},
)
for completion in response.get("results", [])
if "outputText" in completion
]
def parse_streaming_text_completion_response(chunk: dict[str, Any], model_id: str) -> StreamingTextContent:
"""Parse the response from streaming text completion for Amazon Titan models."""
return StreamingTextContent(
choice_index=0,
ai_model_id=model_id,
text=chunk["outputText"],
inner_content=chunk,
metadata={
"usage": CompletionUsage(
prompt_tokens=chunk.get("inputTextTokenCount"),
completion_tokens=chunk.get("totalOutputTextTokenCount"),
)
},
)
# endregion
# region Chat Completion
def get_chat_completion_additional_model_request_fields(
settings: BedrockChatPromptExecutionSettings,
) -> dict[str, Any] | None:
"""Get the additional model request fields for chat completion for Amazon Titan models.
Amazon Titan models do not support additional model request fields.
https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-titan-text.html
"""
return None
# endregion
# region Text Embedding
def get_text_embedding_request_body(text: str, settings: BedrockEmbeddingPromptExecutionSettings) -> dict[str, Any]:
"""Get the request body for text embedding for Amazon Titan models."""
return remove_none_recursively({
"inputText": text,
# Extension data: https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-titan-embed-text.html
"dimensions": settings.extension_data.get("dimensions", None),
"normalize": settings.extension_data.get("normalize", None),
"embeddingTypes": settings.extension_data.get("embeddingTypes", None),
# Extension data: https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-titan-embed-mm.html
"embeddingConfig": settings.extension_data.get("embeddingConfig", None),
})
def parse_text_embedding_response(response: dict[str, Any]) -> list[float]:
"""Parse the response from text embedding for Amazon Titan models."""
if "embedding" not in response or not isinstance(response["embedding"], list):
raise ServiceInvalidResponseError("The response from Amazon Titan model does not contain embeddings.")
return response.get("embedding") # type: ignore
# endregion
@@ -0,0 +1,59 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
from semantic_kernel.connectors.ai.bedrock.bedrock_prompt_execution_settings import (
BedrockChatPromptExecutionSettings,
BedrockTextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.bedrock.services.model_provider.utils import remove_none_recursively
from semantic_kernel.contents.text_content import TextContent
# region Text Completion
def get_text_completion_request_body(prompt: str, settings: BedrockTextPromptExecutionSettings) -> Any:
"""Get the request body for text completion for Anthropic Claude models.
https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-text-completion.html
"""
return remove_none_recursively({
"prompt": f"\n\nHuman:{prompt}\n\nAssistant:",
"temperature": settings.temperature,
"top_p": settings.top_p,
"top_k": settings.top_k,
"max_tokens_to_sample": settings.max_tokens or 200,
"stop_sequences": settings.stop,
})
def parse_text_completion_response(response: dict[str, Any], model_id: str) -> list[TextContent]:
"""Parse the response from text completion for Anthropic Claude models."""
return [
TextContent(
ai_model_id=model_id,
text=response.get("completion", ""),
inner_content=response,
)
]
# endregion
# region Chat Completion
def get_chat_completion_additional_model_request_fields(
settings: BedrockChatPromptExecutionSettings,
) -> dict[str, Any] | None:
"""Get the additional model request fields for chat completion for Anthropic Claude models.
https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html
"""
if settings.top_k is not None:
return {"top_k": settings.top_k}
return None
# endregion
@@ -0,0 +1,101 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
from semantic_kernel.connectors.ai.bedrock.bedrock_prompt_execution_settings import (
BedrockChatPromptExecutionSettings,
BedrockEmbeddingPromptExecutionSettings,
BedrockTextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.bedrock.services.model_provider.utils import remove_none_recursively
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.exceptions.service_exceptions import ServiceInvalidResponseError
# region Text Completion
def get_text_completion_request_body(prompt: str, settings: BedrockTextPromptExecutionSettings) -> Any:
"""Get the request body for text completion for Cohere Command models.
https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-cohere-command.html
"""
return remove_none_recursively({
"prompt": prompt,
"temperature": settings.temperature,
"p": settings.top_p,
"k": settings.top_k,
"max_tokens": settings.max_tokens,
"stop_sequences": settings.stop,
# Extension data
"return_likelihoods": settings.extension_data.get("return_likelihoods", "NONE"),
"num_generations": settings.extension_data.get("num_generations", 1),
"logit_bias": settings.extension_data.get("logit_bias", None),
"truncate": settings.extension_data.get("truncate", "NONE"),
})
def parse_text_completion_response(response: dict[str, Any], model_id: str) -> list[TextContent]:
"""Parse the response from text completion for Anthropic Claude models."""
return [
TextContent(
ai_model_id=model_id,
text=generation["text"],
inner_content=generation,
)
for generation in response.get("generations", [])
]
# endregion
# region Chat Completion
def get_chat_completion_additional_model_request_fields(
settings: BedrockChatPromptExecutionSettings,
) -> dict[str, Any] | None:
"""Get the additional model request fields for chat completion for Cohere Command models.
https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-cohere-command-r-plus.html
"""
additional_fields: dict[str, Any] = remove_none_recursively({
"search_queries_only": settings.extension_data.get("search_queries_only", None),
"preamble": settings.extension_data.get("preamble", None),
"prompt_truncation": settings.extension_data.get("prompt_truncation", None),
"frequency_penalty": settings.extension_data.get("frequency_penalty", None),
"presence_penalty": settings.extension_data.get("presence_penalty", None),
"seed": settings.extension_data.get("seed", None),
"return_prompt": settings.extension_data.get("return_prompt", None),
"raw_prompting": settings.extension_data.get("raw_prompting", None),
})
if not additional_fields:
return None
return additional_fields
# endregion
# region Text Embedding
def get_text_embedding_request_body(text: str, settings: BedrockEmbeddingPromptExecutionSettings) -> Any:
"""Get the request body for text embedding for Cohere Command models."""
return remove_none_recursively({
"texts": [text],
"input_type": settings.extension_data.get("input_type", "search_document"),
"truncate": settings.extension_data.get("truncate", None),
"embedding_types": settings.extension_data.get("embedding_types", None),
})
def parse_text_embedding_response(response: dict[str, Any]) -> list[float]:
"""Parse the response from text embedding for Cohere Command models."""
if "embeddings" not in response or not isinstance(response["embeddings"], list) or len(response["embeddings"]) == 0:
raise ServiceInvalidResponseError("The response from Cohere model does not contain embeddings.")
return response.get("embeddings")[0] # type: ignore
# endregion
@@ -0,0 +1,63 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
from semantic_kernel.connectors.ai.bedrock.bedrock_prompt_execution_settings import (
BedrockChatPromptExecutionSettings,
BedrockTextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.bedrock.services.model_provider.utils import remove_none_recursively
from semantic_kernel.connectors.ai.completion_usage import CompletionUsage
from semantic_kernel.contents.text_content import TextContent
# region Text Completion
def get_text_completion_request_body(prompt: str, settings: BedrockTextPromptExecutionSettings) -> Any:
"""Get the request body for text completion for Meta Llama models.
https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-meta.html
"""
return remove_none_recursively({
"prompt": prompt,
"temperature": settings.temperature,
"topP": settings.top_p,
"max_gen_len": settings.max_tokens,
})
def parse_text_completion_response(response: dict[str, Any], model_id: str) -> list[TextContent]:
"""Parse the response from text completion for Meta Llama models."""
return [
TextContent(
ai_model_id=model_id,
text=response["generation"],
inner_content=response,
metadata={
"usage": CompletionUsage(
prompt_tokens=response.get("prompt_token_count"),
completion_tokens=response.get("completion_token_count"),
)
},
)
]
# endregion
# region Chat Completion
def get_chat_completion_additional_model_request_fields(
settings: BedrockChatPromptExecutionSettings,
) -> dict[str, Any] | None:
"""Get the additional model request fields for chat completion for Meta Llama models.
Meta Llama models do not support additional model request fields.
https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-meta.html
"""
return None
# endregion
@@ -0,0 +1,59 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
from semantic_kernel.connectors.ai.bedrock.bedrock_prompt_execution_settings import (
BedrockChatPromptExecutionSettings,
BedrockTextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.bedrock.services.model_provider.utils import remove_none_recursively
from semantic_kernel.contents.text_content import TextContent
# region Text Completion
def get_text_completion_request_body(prompt: str, settings: BedrockTextPromptExecutionSettings) -> Any:
"""Get the request body for text completion for Mistral AI models.
https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-mistral-text-completion.html
"""
return remove_none_recursively({
"prompt": f"<s>[INST] {prompt} [/INST]",
"max_tokens": settings.max_tokens,
"stop": settings.stop,
"temperature": settings.temperature,
"top_p": settings.top_p,
"top_k": settings.top_k,
})
def parse_text_completion_response(response: dict[str, Any], model_id: str) -> list[TextContent]:
"""Parse the response from text completion for AI21 Labs models."""
return [
TextContent(
ai_model_id=model_id,
text=output["text"],
inner_content=output,
)
for output in response.get("outputs", [])
]
# endregion
# region Chat Completion
def get_chat_completion_additional_model_request_fields(
settings: BedrockChatPromptExecutionSettings,
) -> dict[str, Any] | None:
"""Get the additional model request fields for chat completion for Mistral AI models.
MMistral AI models do not support additional model request fields.
https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-mistral-chat-completion.html
"""
return None
# endregion
@@ -0,0 +1,172 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import Callable
from enum import Enum
from typing import Any
from semantic_kernel.connectors.ai.bedrock.bedrock_prompt_execution_settings import (
BedrockChatPromptExecutionSettings,
BedrockEmbeddingPromptExecutionSettings,
BedrockTextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.bedrock.services.model_provider import (
bedrock_ai21_labs,
bedrock_amazon_titan,
bedrock_anthropic_claude,
bedrock_cohere,
bedrock_meta_llama,
bedrock_mistralai,
)
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
class BedrockModelProvider(Enum):
"""Amazon Bedrock Model Provider Enum.
This list contains the providers of all base models available on Amazon Bedrock.
"""
AI21LABS = "ai21"
AMAZON = "amazon"
ANTHROPIC = "anthropic"
COHERE = "cohere"
META = "meta"
MISTRALAI = "mistral"
@classmethod
def to_model_provider(cls, model_id: str) -> "BedrockModelProvider":
"""Convert a model ID to a model provider."""
try:
return next(provider for provider in cls if provider.value in model_id)
except StopIteration:
raise ValueError(f"Model ID {model_id} does not contain a valid model provider name.")
# region Text Completion
TEXT_COMPLETION_REQUEST_BODY_MAPPING: dict[
BedrockModelProvider, Callable[[str, BedrockTextPromptExecutionSettings], Any]
] = {
BedrockModelProvider.AMAZON: bedrock_amazon_titan.get_text_completion_request_body,
BedrockModelProvider.ANTHROPIC: bedrock_anthropic_claude.get_text_completion_request_body,
BedrockModelProvider.COHERE: bedrock_cohere.get_text_completion_request_body,
BedrockModelProvider.AI21LABS: bedrock_ai21_labs.get_text_completion_request_body,
BedrockModelProvider.META: bedrock_meta_llama.get_text_completion_request_body,
BedrockModelProvider.MISTRALAI: bedrock_mistralai.get_text_completion_request_body,
}
TEXT_COMPLETION_RESPONSE_MAPPING: dict[BedrockModelProvider, Callable[[dict[str, Any], str], list[TextContent]]] = {
BedrockModelProvider.AMAZON: bedrock_amazon_titan.parse_text_completion_response,
BedrockModelProvider.ANTHROPIC: bedrock_anthropic_claude.parse_text_completion_response,
BedrockModelProvider.COHERE: bedrock_cohere.parse_text_completion_response,
BedrockModelProvider.AI21LABS: bedrock_ai21_labs.parse_text_completion_response,
BedrockModelProvider.META: bedrock_meta_llama.parse_text_completion_response,
BedrockModelProvider.MISTRALAI: bedrock_mistralai.parse_text_completion_response,
}
STREAMING_TEXT_COMPLETION_RESPONSE_MAPPING: dict[
BedrockModelProvider, Callable[[dict[str, Any], str], StreamingTextContent]
] = {
BedrockModelProvider.AMAZON: bedrock_amazon_titan.parse_streaming_text_completion_response,
}
def get_text_completion_request_body(
model_id: str,
prompt: str,
settings: BedrockTextPromptExecutionSettings,
model_provider: BedrockModelProvider | None = None,
) -> dict:
"""Get the request body for text completion for Amazon Bedrock models."""
model_provider = model_provider or BedrockModelProvider.to_model_provider(model_id)
return TEXT_COMPLETION_REQUEST_BODY_MAPPING[model_provider](prompt, settings)
def parse_text_completion_response(
model_id: str,
response: dict,
model_provider: BedrockModelProvider | None = None,
) -> list[TextContent]:
"""Parse the response from text completion for Amazon Bedrock models."""
model_provider = model_provider or BedrockModelProvider.to_model_provider(model_id)
return TEXT_COMPLETION_RESPONSE_MAPPING[model_provider](response, model_id)
def parse_streaming_text_completion_response(
model_id: str,
chunk: dict,
model_provider: BedrockModelProvider | None = None,
) -> StreamingTextContent:
"""Parse the response from streaming text completion for Amazon Bedrock models."""
model_provider = model_provider or BedrockModelProvider.to_model_provider(model_id)
return STREAMING_TEXT_COMPLETION_RESPONSE_MAPPING[model_provider](chunk, model_id)
# endregion
# region Chat Completion
CHAT_COMPLETION_ADDITIONAL_MODEL_REQUEST_FIELDS_MAPPING: dict[
BedrockModelProvider, Callable[[BedrockChatPromptExecutionSettings], dict[str, Any] | None]
] = {
BedrockModelProvider.AMAZON: bedrock_amazon_titan.get_chat_completion_additional_model_request_fields,
BedrockModelProvider.ANTHROPIC: bedrock_anthropic_claude.get_chat_completion_additional_model_request_fields,
BedrockModelProvider.COHERE: bedrock_cohere.get_chat_completion_additional_model_request_fields,
BedrockModelProvider.AI21LABS: bedrock_ai21_labs.get_chat_completion_additional_model_request_fields,
BedrockModelProvider.META: bedrock_meta_llama.get_chat_completion_additional_model_request_fields,
BedrockModelProvider.MISTRALAI: bedrock_mistralai.get_chat_completion_additional_model_request_fields,
}
def get_chat_completion_additional_model_request_fields(
model_id: str,
settings: BedrockChatPromptExecutionSettings,
model_provider: BedrockModelProvider | None = None,
) -> dict[str, Any] | None:
"""Get the additional model request fields for chat completion for Amazon Bedrock models."""
model_provider = model_provider or BedrockModelProvider.to_model_provider(model_id)
return CHAT_COMPLETION_ADDITIONAL_MODEL_REQUEST_FIELDS_MAPPING[model_provider](settings)
# endregion
# region Text Embedding
TEXT_EMBEDDING_REQUEST_BODY_MAPPING: dict[
BedrockModelProvider, Callable[[str, BedrockEmbeddingPromptExecutionSettings], Any]
] = {
BedrockModelProvider.AMAZON: bedrock_amazon_titan.get_text_embedding_request_body,
BedrockModelProvider.COHERE: bedrock_cohere.get_text_embedding_request_body,
}
TEXT_EMBEDDING_RESPONSE_MAPPING: dict[BedrockModelProvider, Callable[[dict], list[float]]] = {
BedrockModelProvider.AMAZON: bedrock_amazon_titan.parse_text_embedding_response,
BedrockModelProvider.COHERE: bedrock_cohere.parse_text_embedding_response,
}
def get_text_embedding_request_body(
model_id: str,
text: str,
settings: BedrockEmbeddingPromptExecutionSettings,
model_provider: BedrockModelProvider | None = None,
) -> dict:
"""Get the request body for text embedding for Amazon Bedrock models."""
model_provider = model_provider or BedrockModelProvider.to_model_provider(model_id)
return TEXT_EMBEDDING_REQUEST_BODY_MAPPING[model_provider](text, settings)
def parse_text_embedding_response(
model_id: str,
response: dict,
model_provider: BedrockModelProvider | None = None,
) -> list[float]:
"""Parse the response from text embedding for Amazon Bedrock models."""
model_provider = model_provider or BedrockModelProvider.to_model_provider(model_id)
return TEXT_EMBEDDING_RESPONSE_MAPPING[model_provider](response)
# endregion
@@ -0,0 +1,217 @@
# Copyright (c) Microsoft. All rights reserved.
import json
from collections.abc import Callable, Mapping
from typing import TYPE_CHECKING, Any
from semantic_kernel.connectors.ai.bedrock.bedrock_prompt_execution_settings import BedrockChatPromptExecutionSettings
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType
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
from semantic_kernel.contents.utils.finish_reason import FinishReason
from semantic_kernel.exceptions.service_exceptions import ServiceInvalidRequestError
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
def remove_none_recursively(data: dict, max_depth: int = 5) -> dict:
"""Remove None values from a dictionary recursively."""
if max_depth <= 0:
return data
if not isinstance(data, dict):
return data
return {k: remove_none_recursively(v, max_depth=max_depth - 1) for k, v in data.items() if v is not None}
def _format_system_message(message: ChatMessageContent) -> dict[str, str]:
"""Format a system message to the expected object for the client.
Note that Guardrails are currently not supported.
Args:
message: The system message.
Returns:
The formatted system message.
"""
return {"text": message.content}
def _format_user_message(message: ChatMessageContent) -> dict[str, Any]:
"""Format a user message to the expected object for the client.
Note that Guardrails and Documents are currently not supported.
Args:
message: The user message.
Returns:
The formatted user message.
"""
contents: list[Any] = []
for item in message.items:
if not isinstance(item, (ImageContent, TextContent)):
raise ServiceInvalidRequestError("Only text and image content are supported in a user message.")
if isinstance(item, ImageContent):
contents.append({
"image": {
"format": item.mime_type.removeprefix("image/"),
"source": {
"bytes": item.data,
},
}
})
else:
contents.append({"text": item.text})
return {
"role": "user",
"content": contents,
}
def _format_assistant_message(message: ChatMessageContent) -> dict[str, Any]:
"""Format an assistant message to the expected object for the client.
Note that Guardrails and documents are currently not supported.
Args:
message: The assistant message.
Returns:
The formatted assistant message.
"""
contents: list[Any] = []
for item in message.items:
if isinstance(item, ImageContent):
raise ServiceInvalidRequestError("Image content is not supported in an assistant message.")
if isinstance(item, TextContent):
contents.append({"text": item.text})
elif isinstance(item, FunctionCallContent):
contents.append({
"toolUse": {
"toolUseId": item.id,
"name": item.name,
"input": item.arguments
if isinstance(item.arguments, Mapping)
else json.loads(item.arguments or "{}"),
}
})
else:
raise ServiceInvalidRequestError(f"Unsupported content type in an assistant message: {type(item)}")
return {
"role": "assistant",
"content": contents,
}
def _format_tool_message(message: ChatMessageContent) -> dict[str, Any]:
"""Format a tool message to the expected object for the client.
Args:
message: The tool message.
Returns:
The formatted tool message.
"""
contents: list[Any] = []
for item in message.items:
if isinstance(item, ImageContent):
raise ServiceInvalidRequestError("Image content is not supported in a tool message.")
if isinstance(item, TextContent):
contents.append({"text": item.text})
elif isinstance(item, FunctionResultContent):
contents.append({
"toolResult": {
"toolUseId": item.id,
# Image and document content are not yet supported in a tool message by SK
"content": [{"text": str(item)}],
}
})
else:
raise ServiceInvalidRequestError(f"Unsupported content type in a tool message: {type(item)}")
return {
"role": "user",
"content": contents,
}
MESSAGE_CONVERTERS: dict[AuthorRole, Callable[[ChatMessageContent], dict[str, Any]]] = {
AuthorRole.SYSTEM: _format_system_message,
AuthorRole.USER: _format_user_message,
AuthorRole.ASSISTANT: _format_assistant_message,
AuthorRole.TOOL: _format_tool_message,
}
def update_settings_from_function_choice_configuration(
function_choice_configuration: "FunctionCallChoiceConfiguration",
settings: "PromptExecutionSettings",
type: FunctionChoiceType,
) -> None:
"""Update the settings from a FunctionChoiceConfiguration."""
assert isinstance(settings, BedrockChatPromptExecutionSettings) # nosec
# Bedrock supports 3 types of tool choice behavior: auto, any, tool
# We will map our `FunctionChoiceType` to the corresponding Bedrock type following these rules:
# `FunctionChoiceType.NONE` -> No configuration needed
# `FunctionChoiceType.AUTO` -> "auto"
# `FunctionChoiceType.REQUIRED`:
# - If there are more than one available functions -> "any"
# - If there is only one available function -> "tool"
if type == FunctionChoiceType.NONE:
return
if function_choice_configuration.available_functions:
if type == FunctionChoiceType.AUTO:
settings.tool_choice = {"auto": {}}
elif type == FunctionChoiceType.REQUIRED:
if len(function_choice_configuration.available_functions) > 1:
settings.tool_choice = {"any": {}}
else:
settings.tool_choice = {
"tool": {
"name": function_choice_configuration.available_functions[0].fully_qualified_name,
}
}
settings.tools = [
{
"toolSpec": {
"name": function.fully_qualified_name,
"description": function.description or "",
"inputSchema": {
"json": {
"type": "object",
"properties": {param.name: param.schema_data for param in function.parameters},
"required": [p.name for p in function.parameters if p.is_required],
}
},
}
}
for function in function_choice_configuration.available_functions
]
def finish_reason_from_bedrock_to_semantic_kernel(finish_reason: str) -> FinishReason | None:
"""Convert a finish reason from Bedrock to Semantic Kernel."""
return {
"stop_sequence": FinishReason.STOP,
"end_turn": FinishReason.STOP,
"max_tokens": FinishReason.LENGTH,
"content_filtered": FinishReason.CONTENT_FILTER,
"tool_use": FinishReason.TOOL_CALLS,
}.get(finish_reason)
@@ -0,0 +1,443 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import copy
import logging
from abc import ABC
from collections.abc import AsyncGenerator, Callable
from functools import reduce
from typing import TYPE_CHECKING, Any, ClassVar
from opentelemetry.trace import Span, Tracer, get_tracer, use_span
from pydantic import Field
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType
from semantic_kernel.const import AUTO_FUNCTION_INVOCATION_SPAN_NAME
from semantic_kernel.contents.annotation_content import AnnotationContent
from semantic_kernel.contents.file_reference_content import FileReferenceContent
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.exceptions.service_exceptions import ServiceInvalidExecutionSettingsError
from semantic_kernel.services.ai_service_client_base import AIServiceClientBase
from semantic_kernel.utils.telemetry.model_diagnostics.gen_ai_attributes import AVAILABLE_FUNCTIONS
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
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.kernel import Kernel
logger: logging.Logger = logging.getLogger(__name__)
tracer: Tracer = get_tracer(__name__)
class ChatCompletionClientBase(AIServiceClientBase, ABC):
"""Base class for chat completion AI services."""
# Connectors that support function calling should set this to True
SUPPORTS_FUNCTION_CALLING: ClassVar[bool] = False
instruction_role: str = Field(default_factory=lambda: "system", description="The role for instructions.")
# region Internal methods to be implemented by the derived classes
async def _inner_get_chat_message_contents(
self,
chat_history: "ChatHistory",
settings: "PromptExecutionSettings",
) -> list["ChatMessageContent"]:
"""Send a chat request to the AI service.
Args:
chat_history (ChatHistory): The chat history to send.
settings (PromptExecutionSettings): The settings for the request.
Returns:
chat_message_contents (list[ChatMessageContent]): The chat message contents representing the response(s).
"""
raise NotImplementedError("The _inner_get_chat_message_contents method is not implemented.")
async def _inner_get_streaming_chat_message_contents(
self,
chat_history: "ChatHistory",
settings: "PromptExecutionSettings",
function_invoke_attempt: int = 0,
) -> AsyncGenerator[list["StreamingChatMessageContent"], Any]:
"""Send a streaming chat request to the AI service.
Args:
chat_history: The chat history to send.
settings: The settings for the request.
function_invoke_attempt: The current attempt count for automatically invoking functions.
Yields:
streaming_chat_message_contents: The streaming chat message contents.
"""
raise NotImplementedError("The _inner_get_streaming_chat_message_contents method is not implemented.")
# Below is needed for mypy: https://mypy.readthedocs.io/en/stable/more_types.html#asynchronous-iterators
if False:
yield
# endregion
# region Public methods
async def get_chat_message_contents(
self,
chat_history: "ChatHistory",
settings: "PromptExecutionSettings",
**kwargs: Any,
) -> list["ChatMessageContent"]:
"""Create chat message contents, in the number specified by the settings.
Args:
chat_history (ChatHistory): A list of chats in a chat_history object, that can be
rendered into messages from system, user, assistant and tools.
settings (PromptExecutionSettings): Settings for the request.
**kwargs (Any): The optional arguments.
Returns:
A list of chat message contents representing the response(s) from the LLM.
"""
from semantic_kernel.connectors.ai.function_calling_utils import (
merge_function_results,
)
# Create a copy of the settings to avoid modifying the original settings
settings = copy.deepcopy(settings)
# Later on, we already use the tools or equivalent settings, we cast here.
if not isinstance(settings, self.get_prompt_execution_settings_class()):
settings = self.get_prompt_execution_settings_from_settings(settings)
if not self.SUPPORTS_FUNCTION_CALLING:
return await self._inner_get_chat_message_contents(chat_history, settings)
kernel: "Kernel" = kwargs.get("kernel") # type: ignore
if settings.function_choice_behavior is not None:
if kernel is None:
raise ServiceInvalidExecutionSettingsError("The kernel is required for function calls.")
self._verify_function_choice_settings(settings)
if settings.function_choice_behavior and kernel:
# Configure the function choice behavior into the settings object
# that will become part of the request to the AI service
settings.function_choice_behavior.configure(
kernel=kernel,
update_settings_callback=self._update_function_choice_settings_callback(),
settings=settings,
)
if (
settings.function_choice_behavior is None
or not settings.function_choice_behavior.auto_invoke_kernel_functions
):
return await self._inner_get_chat_message_contents(chat_history, settings)
# Auto invoke loop
with use_span(self._start_auto_function_invocation_activity(kernel, settings), end_on_exit=True) as _:
for request_index in range(settings.function_choice_behavior.maximum_auto_invoke_attempts):
completions = await self._inner_get_chat_message_contents(chat_history, settings)
# Get the function call contents from the chat message. There is only one chat message,
# which should be checked in the `_verify_function_choice_settings` method.
function_calls = [item for item in completions[0].items if isinstance(item, FunctionCallContent)]
if (fc_count := len(function_calls)) == 0:
return completions
# Since we have a function call, add the assistant's tool call message to the history
chat_history.add_message(message=completions[0])
logger.info(f"processing {fc_count} tool calls in parallel.")
# This function either updates the chat history with the function call results
# or returns the context, with terminate set to True in which case the loop will
# break and the function calls are returned.
results = await asyncio.gather(
*[
kernel.invoke_function_call(
function_call=function_call,
chat_history=chat_history,
arguments=kwargs.get("arguments"),
execution_settings=settings,
function_call_count=fc_count,
request_index=request_index,
function_behavior=settings.function_choice_behavior,
)
for function_call in function_calls
],
)
if any(result.terminate for result in results if result is not None):
return merge_function_results(chat_history.messages[-len(results) :])
else:
# Do a final call, without function calling when the max has been reached.
self._reset_function_choice_settings(settings)
return await self._inner_get_chat_message_contents(chat_history, settings)
async def get_chat_message_content(
self, chat_history: "ChatHistory", settings: "PromptExecutionSettings", **kwargs: Any
) -> "ChatMessageContent | None":
"""This is the method that is called from the kernel to get a response from a chat-optimized LLM.
Args:
chat_history (ChatHistory): A list of chat chat_history, that can be rendered into a
set of chat_history, from system, user, assistant and function.
settings (PromptExecutionSettings): Settings for the request.
kwargs (Dict[str, Any]): The optional arguments.
Returns:
A string representing the response from the LLM.
"""
results = await self.get_chat_message_contents(chat_history=chat_history, settings=settings, **kwargs)
if results:
return results[0]
# this should not happen, should error out before returning an empty list
return None # pragma: no cover
async def get_streaming_chat_message_contents(
self,
chat_history: "ChatHistory",
settings: "PromptExecutionSettings",
**kwargs: Any,
) -> AsyncGenerator[list["StreamingChatMessageContent"], Any]:
"""Create streaming chat message contents, in the number specified by the settings.
Args:
chat_history (ChatHistory): A list of chat chat_history, that can be rendered into a
set of chat_history, from system, user, assistant and function.
settings (PromptExecutionSettings): Settings for the request.
kwargs (Dict[str, Any]): The optional arguments.
Yields:
A stream representing the response(s) from the LLM.
"""
from semantic_kernel.connectors.ai.function_calling_utils import (
merge_streaming_function_results,
)
# Create a copy of the settings to avoid modifying the original settings
settings = copy.deepcopy(settings)
# Later on, we already use the tools or equivalent settings, we cast here.
if not isinstance(settings, self.get_prompt_execution_settings_class()):
settings = self.get_prompt_execution_settings_from_settings(settings)
if not self.SUPPORTS_FUNCTION_CALLING:
async for streaming_chat_message_contents in self._inner_get_streaming_chat_message_contents(
chat_history, settings
):
yield streaming_chat_message_contents
return
kernel: "Kernel" = kwargs.get("kernel") # type: ignore
if settings.function_choice_behavior is not None:
if kernel is None:
raise ServiceInvalidExecutionSettingsError("The kernel is required for function calls.")
self._verify_function_choice_settings(settings)
if settings.function_choice_behavior and kernel:
# Configure the function choice behavior into the settings object
# that will become part of the request to the AI service
settings.function_choice_behavior.configure(
kernel=kernel,
update_settings_callback=self._update_function_choice_settings_callback(),
settings=settings,
)
if (
settings.function_choice_behavior is None
or not settings.function_choice_behavior.auto_invoke_kernel_functions
):
async for streaming_chat_message_contents in self._inner_get_streaming_chat_message_contents(
chat_history, settings
):
yield streaming_chat_message_contents
return
# Auto invoke loop
with use_span(self._start_auto_function_invocation_activity(kernel, settings), end_on_exit=True) as _:
for request_index in range(settings.function_choice_behavior.maximum_auto_invoke_attempts):
# Hold the messages, if there are more than one response, it will not be used, so we flatten
all_messages: list["StreamingChatMessageContent"] = []
function_call_returned = False
async for messages in self._inner_get_streaming_chat_message_contents(
chat_history, settings, request_index
):
for msg in messages:
if msg is not None:
all_messages.append(msg)
if not function_call_returned and any(
isinstance(item, FunctionCallContent) for item in msg.items
):
function_call_returned = True
yield messages
if not function_call_returned:
return
# There is one FunctionCallContent response stream in the messages, combining now to create
# the full completion depending on the prompt, the message may contain both function call
# content and others
full_completion: StreamingChatMessageContent = reduce(lambda x, y: x + y, all_messages)
function_calls = [item for item in full_completion.items if isinstance(item, FunctionCallContent)]
chat_history.add_message(message=full_completion)
fc_count = len(function_calls)
logger.info(f"processing {fc_count} tool calls in parallel.")
# This function either updates the chat history with the function call results
# or returns the context, with terminate set to True in which case the loop will
# break and the function calls are returned.
results = await asyncio.gather(
*[
kernel.invoke_function_call(
function_call=function_call,
chat_history=chat_history,
arguments=kwargs.get("arguments"),
is_streaming=True,
execution_settings=settings,
function_call_count=fc_count,
request_index=request_index,
function_behavior=settings.function_choice_behavior,
)
for function_call in function_calls
],
)
# Merge and yield the function results, regardless of the termination status
# Include the ai_model_id so we can later add two streaming messages together
# Some settings may not have an ai_model_id, so we need to check for it
ai_model_id = self._get_ai_model_id(settings)
function_result_messages = merge_streaming_function_results(
messages=chat_history.messages[-len(results) :],
ai_model_id=ai_model_id, # type: ignore
function_invoke_attempt=request_index,
)
if self._yield_function_result_messages(function_result_messages):
yield function_result_messages
if any(result.terminate for result in results if result is not None):
break
async def get_streaming_chat_message_content(
self,
chat_history: "ChatHistory",
settings: "PromptExecutionSettings",
**kwargs: Any,
) -> AsyncGenerator["StreamingChatMessageContent | None", Any]:
"""This is the method that is called from the kernel to get a stream response from a chat-optimized LLM.
Args:
chat_history (ChatHistory): A list of chat chat_history, that can be rendered into a
set of chat_history, from system, user, assistant and function.
settings (PromptExecutionSettings): Settings for the request.
kwargs (Dict[str, Any]): The optional arguments.
Yields:
A stream representing the response(s) from the LLM.
"""
async for streaming_chat_message_contents in self.get_streaming_chat_message_contents(
chat_history, settings, **kwargs
):
if streaming_chat_message_contents:
yield streaming_chat_message_contents[0]
else:
# this should not happen, should error out before returning an empty list
yield None # pragma: no cover
# endregion
# region internal handlers
def _prepare_chat_history_for_request(
self,
chat_history: "ChatHistory",
role_key: str = "role",
content_key: str = "content",
) -> Any:
"""Prepare the chat history for a request.
Allowing customization of the key names for role/author, and optionally overriding the role.
ChatRole.TOOL messages need to be formatted different than system/user/assistant messages:
They require a "tool_call_id" and (function) "name" key, and the "metadata" key should
be removed. The "encoding" key should also be removed.
Override this method to customize the formatting of the chat history for a request.
Args:
chat_history (ChatHistory): The chat history to prepare.
role_key (str): The key name for the role/author.
content_key (str): The key name for the content/message.
Returns:
prepared_chat_history (Any): The prepared chat history for a request.
"""
return [
message.to_dict(role_key=role_key, content_key=content_key)
for message in chat_history.messages
if not isinstance(message, (AnnotationContent, FileReferenceContent))
]
def _verify_function_choice_settings(self, settings: "PromptExecutionSettings") -> None:
"""Additional verification to validate settings for function choice behavior.
Override this method to add additional verification for the settings.
Args:
settings (PromptExecutionSettings): The settings to verify.
"""
return
def _update_function_choice_settings_callback(
self,
) -> Callable[["FunctionCallChoiceConfiguration", "PromptExecutionSettings", FunctionChoiceType], None]:
"""Return the callback function to update the settings from a function call configuration.
Override this method to provide a custom callback function to
update the settings from a function call configuration.
"""
return lambda configuration, settings, choice_type: None
def _reset_function_choice_settings(self, settings: "PromptExecutionSettings") -> None:
"""Reset the settings updated by `_update_function_choice_settings_callback`.
Override this method to reset the settings updated by `_update_function_choice_settings_callback`.
Args:
settings (PromptExecutionSettings): The prompt execution settings to reset.
"""
return
def _start_auto_function_invocation_activity(self, kernel: "Kernel", settings: "PromptExecutionSettings") -> Span:
"""Start the auto function invocation activity.
Args:
kernel (Kernel): The kernel instance.
settings (PromptExecutionSettings): The prompt execution settings.
"""
span = tracer.start_span(AUTO_FUNCTION_INVOCATION_SPAN_NAME)
if settings.function_choice_behavior is not None:
available_functions = settings.function_choice_behavior.get_config(kernel).available_functions or []
span.set_attribute(
AVAILABLE_FUNCTIONS,
",".join([f.fully_qualified_name for f in available_functions]),
)
return span
def _get_ai_model_id(self, settings: "PromptExecutionSettings") -> str:
"""Retrieve the AI model ID from settings if available.
Attempt to get ai_model_id from the settings object. If it doesn't exist or
is blank, fallback to self.ai_model_id (from AIServiceClientBase).
"""
return getattr(settings, "ai_model_id", self.ai_model_id) or self.ai_model_id
def _yield_function_result_messages(self, function_result_messages: list) -> bool:
"""Determine if the function result messages should be yielded.
If there are messages and if the first message has items, then yield the messages.
"""
return len(function_result_messages) > 0 and len(function_result_messages[0].items) > 0
# endregion
@@ -0,0 +1,56 @@
# Copyright (c) Microsoft. All rights reserved.
from openai.types import CompletionUsage as OpenAICompletionUsage
from openai.types.completion_usage import CompletionTokensDetails, PromptTokensDetails
from semantic_kernel.kernel_pydantic import KernelBaseModel
class CompletionUsage(KernelBaseModel):
"""A class representing the usage of tokens in a completion request."""
prompt_tokens: int | None = None
prompt_tokens_details: PromptTokensDetails | None = None
completion_tokens: int | None = None
completion_tokens_details: CompletionTokensDetails | None = None
@classmethod
def from_openai(cls, openai_completion_usage: OpenAICompletionUsage):
"""Create a CompletionUsage instance from an OpenAICompletionUsage instance."""
return cls(
prompt_tokens=openai_completion_usage.prompt_tokens,
prompt_tokens_details=openai_completion_usage.prompt_tokens_details
if openai_completion_usage.prompt_tokens_details
else None,
completion_tokens=openai_completion_usage.completion_tokens,
completion_tokens_details=openai_completion_usage.completion_tokens_details
if openai_completion_usage.completion_tokens_details
else None,
)
def __add__(self, other: "CompletionUsage") -> "CompletionUsage":
"""Combine two CompletionUsage instances by summing their token counts."""
def _merge_details(cls, a, b):
"""Merge two details objects by summing their fields."""
if a is None and b is None:
return None
kwargs = {}
for field in cls.__annotations__:
x = getattr(a, field, None)
y = getattr(b, field, None)
value = None if x is None and y is None else (x or 0) + (y or 0)
kwargs[field] = value
return cls(**kwargs)
return CompletionUsage(
prompt_tokens=(self.prompt_tokens or 0) + (other.prompt_tokens or 0),
completion_tokens=(self.completion_tokens or 0) + (other.completion_tokens or 0),
prompt_tokens_details=_merge_details(
PromptTokensDetails, self.prompt_tokens_details, other.prompt_tokens_details
),
completion_tokens_details=_merge_details(
CompletionTokensDetails, self.completion_tokens_details, other.completion_tokens_details
),
)
@@ -0,0 +1,50 @@
# Copyright (c) Microsoft. All rights reserved.
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any
from semantic_kernel.services.ai_service_client_base import AIServiceClientBase
from semantic_kernel.utils.feature_stage_decorator import experimental
if TYPE_CHECKING:
from numpy import ndarray
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
@experimental
class EmbeddingGeneratorBase(AIServiceClientBase, ABC):
"""Base class for embedding generators."""
@abstractmethod
async def generate_embeddings(
self,
texts: list[str],
settings: "PromptExecutionSettings | None" = None,
**kwargs: Any,
) -> "ndarray":
"""Returns embeddings for the given texts as ndarray.
Args:
texts (List[str]): The texts to generate embeddings for.
settings (PromptExecutionSettings): The settings to use for the request, optional.
kwargs (Any): Additional arguments to pass to the request.
"""
pass
async def generate_raw_embeddings(
self,
texts: list[str],
settings: "PromptExecutionSettings | None" = None,
**kwargs: Any,
) -> Any:
"""Returns embeddings for the given texts in the unedited format.
This is not implemented for all embedding services, falling back to the generate_embeddings method.
Args:
texts (List[str]): The texts to generate embeddings for.
settings (PromptExecutionSettings): The settings to use for the request, optional.
kwargs (Any): Additional arguments to pass to the request.
"""
return await self.generate_embeddings(texts, settings, **kwargs)
@@ -0,0 +1,21 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase as NewEmbeddingGeneratorBase
from semantic_kernel.utils.feature_stage_decorator import experimental
if sys.version_info >= (3, 13):
from warnings import deprecated
else:
from typing_extensions import deprecated
@deprecated(
"This class has been moved to semantic_kernel.connectors.ai.embedding_generator_base. Please update your imports."
)
@experimental
class EmbeddingGeneratorBase(NewEmbeddingGeneratorBase):
"""Base class for embedding generators."""
pass
@@ -0,0 +1,15 @@
# Copyright (c) Microsoft. All rights reserved.
from pydantic.dataclasses import dataclass
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
from semantic_kernel.utils.feature_stage_decorator import experimental
@experimental
@dataclass
class FunctionCallChoiceConfiguration:
"""Configuration for function call choice."""
available_functions: list[KernelFunctionMetadata] | None = None
@@ -0,0 +1,191 @@
# Copyright (c) Microsoft. All rights reserved.
from collections import OrderedDict
from collections.abc import Callable
from copy import deepcopy
from typing import TYPE_CHECKING, Any
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
from semantic_kernel.utils.feature_stage_decorator import experimental
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.function_choice_behavior import (
FunctionCallChoiceConfiguration,
FunctionChoiceType,
)
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
from semantic_kernel.kernel import Kernel
def update_settings_from_function_call_configuration(
function_choice_configuration: "FunctionCallChoiceConfiguration",
settings: "PromptExecutionSettings",
type: "FunctionChoiceType",
) -> None:
"""Update the settings from a FunctionChoiceConfiguration."""
if (
function_choice_configuration.available_functions
and hasattr(settings, "tool_choice")
and hasattr(settings, "tools")
):
settings.tool_choice = type
settings.tools = [
kernel_function_metadata_to_function_call_format(f)
for f in function_choice_configuration.available_functions
]
def kernel_function_metadata_to_function_call_format(
metadata: "KernelFunctionMetadata",
) -> dict[str, Any]:
"""Convert the kernel function metadata to function calling format."""
return {
"type": "function",
"function": {
"name": metadata.fully_qualified_name,
"description": metadata.description or "",
"parameters": {
"type": "object",
"properties": {
param.name: param.schema_data for param in metadata.parameters if param.include_in_function_choices
},
"required": [p.name for p in metadata.parameters if p.is_required and p.include_in_function_choices],
},
},
}
def kernel_function_metadata_to_response_function_call_format(
metadata: "KernelFunctionMetadata",
) -> dict[str, Any]:
"""Convert the kernel function metadata to function calling format."""
return {
"type": "function",
"name": metadata.fully_qualified_name,
"description": metadata.description or "",
"parameters": {
"type": "object",
"properties": {
param.name: param.schema_data for param in metadata.parameters if param.include_in_function_choices
},
"required": [p.name for p in metadata.parameters if p.is_required and p.include_in_function_choices],
},
}
def _combine_filter_dicts(*dicts: dict[str, list[str]]) -> dict:
"""Combine multiple filter dictionaries with list values into one dictionary.
This method is ensuring unique values while preserving order.
"""
combined_filters = {}
keys = set().union(*(d.keys() for d in dicts))
for key in keys:
combined_functions: OrderedDict[str, None] = OrderedDict()
for d in dicts:
if key in d:
if isinstance(d[key], list):
for item in d[key]:
combined_functions[item] = None
else:
raise ServiceInitializationError(f"Values for filter key '{key}' are not lists.")
combined_filters[key] = list(combined_functions.keys())
return combined_filters
def merge_function_results(
messages: list["ChatMessageContent"],
) -> list["ChatMessageContent"]:
"""Combine multiple function result content types to one chat message content type.
This method combines the FunctionResultContent items from separate ChatMessageContent messages,
and is used in the event that the `context.terminate = True` condition is met.
"""
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.function_result_content import FunctionResultContent
items: list[Any] = []
for message in messages:
items.extend([item for item in message.items if isinstance(item, FunctionResultContent)])
return [
ChatMessageContent(
role=AuthorRole.TOOL,
items=items,
)
]
def merge_streaming_function_results(
messages: list["ChatMessageContent | StreamingChatMessageContent"],
ai_model_id: str | None = None,
function_invoke_attempt: int | None = None,
) -> list["StreamingChatMessageContent"]:
"""Combine multiple streaming function result content types to one streaming chat message content type.
This method combines the FunctionResultContent items from separate StreamingChatMessageContent messages,
and is used in the event that the `context.terminate = True` condition is met.
Args:
messages: The list of streaming chat message content types.
ai_model_id: The AI model ID.
function_invoke_attempt: The function invoke attempt.
Returns:
The combined streaming chat message content type.
"""
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
items: list[Any] = []
for message in messages:
items.extend([item for item in message.items if isinstance(item, FunctionResultContent)])
return [
StreamingChatMessageContent(
role=AuthorRole.TOOL,
items=items,
choice_index=0,
ai_model_id=ai_model_id,
function_invoke_attempt=function_invoke_attempt,
)
]
@experimental
def prepare_settings_for_function_calling(
settings: "PromptExecutionSettings",
settings_class: type["PromptExecutionSettings"],
update_settings_callback: Callable[..., None],
kernel: "Kernel",
) -> "PromptExecutionSettings":
"""Prepare settings for the service.
Args:
settings: Prompt execution settings.
settings_class: The settings class.
update_settings_callback: The callback to update the settings.
kernel: Kernel instance.
Returns:
PromptExecutionSettings of type settings_class.
"""
settings = deepcopy(settings)
if not isinstance(settings, settings_class):
settings = settings_class.from_prompt_execution_settings(settings)
if settings.function_choice_behavior:
# Configure the function choice behavior into the settings object
# that will become part of the request to the AI service
settings.function_choice_behavior.configure(
kernel=kernel,
update_settings_callback=update_settings_callback,
settings=settings,
)
return settings
@@ -0,0 +1,224 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
from collections.abc import Callable
from typing import TYPE_CHECKING, Literal, TypeVar
from semantic_kernel.connectors.ai.function_choice_type import FunctionChoiceType
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
from semantic_kernel.kernel_pydantic import KernelBaseModel
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
from semantic_kernel.kernel import Kernel
DEFAULT_MAX_AUTO_INVOKE_ATTEMPTS = 5
logger = logging.getLogger(__name__)
_T = TypeVar("_T", bound="FunctionChoiceBehavior")
@experimental
class FunctionChoiceBehavior(KernelBaseModel):
"""Class that controls function choice behavior.
Attributes:
enable_kernel_functions: Enable kernel functions.
max_auto_invoke_attempts: The maximum number of auto invoke attempts.
filters: Filters for the function choice behavior. Available options are: excluded_plugins,
included_plugins, excluded_functions, or included_functions.
type_: The type of function choice behavior.
Properties:
auto_invoke_kernel_functions: Check if the kernel functions should be auto-invoked.
Determined as max_auto_invoke_attempts > 0.
Methods:
configure: Configures the settings for the function call behavior,
the default version in this class, does nothing, use subclasses for different behaviors.
Class methods:
Auto: Returns FunctionChoiceBehavior class with auto_invoke enabled, and the desired functions
based on either the specified filters or the full qualified names. The model will decide which function
to use, if any.
NoneInvoke: Returns FunctionChoiceBehavior class with auto_invoke disabled, and the desired functions
based on either the specified filters or the full qualified names. The model does not invoke any functions,
but can rather describe how it would invoke a function to complete a given task/query.
Required: Returns FunctionChoiceBehavior class with auto_invoke enabled, and the desired functions
based on either the specified filters or the full qualified names. The model is required to use one of the
provided functions to complete a given task/query.
"""
enable_kernel_functions: bool = True
maximum_auto_invoke_attempts: int = DEFAULT_MAX_AUTO_INVOKE_ATTEMPTS
filters: (
dict[Literal["excluded_plugins", "included_plugins", "excluded_functions", "included_functions"], list[str]]
| None
) = None
type_: FunctionChoiceType | None = None
@property
def auto_invoke_kernel_functions(self):
"""Return True if auto_invoke_kernel_functions is enabled."""
return self.maximum_auto_invoke_attempts > 0
@auto_invoke_kernel_functions.setter
def auto_invoke_kernel_functions(self, value: bool):
"""Set the auto_invoke_kernel_functions property."""
self.maximum_auto_invoke_attempts = DEFAULT_MAX_AUTO_INVOKE_ATTEMPTS if value else 0
def _check_and_get_config(
self,
kernel: "Kernel",
filters: dict[
Literal["excluded_plugins", "included_plugins", "excluded_functions", "included_functions"], list[str]
]
| None = None,
) -> "FunctionCallChoiceConfiguration":
"""Check for missing functions and get the function call choice configuration."""
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
if filters:
return FunctionCallChoiceConfiguration(available_functions=kernel.get_list_of_function_metadata(filters))
return FunctionCallChoiceConfiguration(available_functions=kernel.get_full_list_of_function_metadata())
def configure(
self,
kernel: "Kernel",
update_settings_callback: Callable[..., None],
settings: "PromptExecutionSettings",
) -> None:
"""Configure the function choice behavior."""
if not self.enable_kernel_functions:
return
config = self.get_config(kernel)
if config:
update_settings_callback(config, settings, self.type_)
def get_config(self, kernel: "Kernel") -> "FunctionCallChoiceConfiguration":
"""Get the function call choice configuration based on the type."""
return self._check_and_get_config(kernel, self.filters)
@classmethod
def Auto(
cls: type[_T],
auto_invoke: bool = True,
*,
filters: dict[
Literal["excluded_plugins", "included_plugins", "excluded_functions", "included_functions"], list[str]
]
| None = None,
**kwargs,
) -> _T:
"""Creates a FunctionChoiceBehavior with type AUTO.
Returns FunctionChoiceBehavior class with auto_invoke enabled, and the desired functions
based on either the specified filters or the full qualified names. The model will decide which function
to use, if any.
"""
kwargs.setdefault("maximum_auto_invoke_attempts", DEFAULT_MAX_AUTO_INVOKE_ATTEMPTS if auto_invoke else 0)
return cls(
type_=FunctionChoiceType.AUTO,
filters=filters,
**kwargs,
)
@classmethod
def NoneInvoke(
cls: type[_T],
*,
filters: dict[
Literal["excluded_plugins", "included_plugins", "excluded_functions", "included_functions"], list[str]
]
| None = None,
**kwargs,
) -> _T:
"""Creates a FunctionChoiceBehavior with type NONE.
Returns FunctionChoiceBehavior class with auto_invoke disabled, and the desired functions
based on either the specified filters or the full qualified names. The model does not invoke any functions,
but can rather describe how it would invoke a function to complete a given task/query.
"""
kwargs.setdefault("maximum_auto_invoke_attempts", 0)
return cls(
type_=FunctionChoiceType.NONE,
filters=filters,
**kwargs,
)
@classmethod
def Required(
cls: type[_T],
auto_invoke: bool = True,
*,
filters: dict[
Literal["excluded_plugins", "included_plugins", "excluded_functions", "included_functions"], list[str]
]
| None = None,
**kwargs,
) -> _T:
"""Creates a FunctionChoiceBehavior with type REQUIRED.
Returns FunctionChoiceBehavior class with auto_invoke enabled, and the desired functions
based on either the specified filters or the full qualified names. The model is required to use one of the
provided functions to complete a given task/query.
"""
kwargs.setdefault("maximum_auto_invoke_attempts", 1 if auto_invoke else 0)
return cls(
type_=FunctionChoiceType.REQUIRED,
filters=filters,
**kwargs,
)
@classmethod
def from_dict(cls: type[_T], data: dict) -> _T:
"""Create a FunctionChoiceBehavior from a dictionary."""
from semantic_kernel.connectors.ai.function_calling_utils import _combine_filter_dicts
type_map = {
"auto": cls.Auto,
"none": cls.NoneInvoke,
"required": cls.Required,
}
behavior_type = data.pop("type", "auto")
auto_invoke = data.pop("auto_invoke", False)
functions = data.pop("functions", None)
filters = data.pop("filters", None)
if functions:
valid_fqns = [name.replace(".", "-") for name in functions]
if filters:
filters = _combine_filter_dicts(filters, {"included_functions": valid_fqns})
else:
filters = {"included_functions": valid_fqns}
return type_map[behavior_type]( # type: ignore
auto_invoke=auto_invoke,
filters=filters,
**data,
)
@classmethod
def from_string(cls: type[_T], data: str) -> _T:
"""Create a FunctionChoiceBehavior from a string.
This method converts the provided string to a FunctionChoiceBehavior object
for the specified type.
"""
type_value = data.lower()
if type_value == "auto":
return cls.Auto()
if type_value == "none":
return cls.NoneInvoke()
if type_value == "required":
return cls.Required()
raise ServiceInitializationError(
f"The specified type `{type_value}` is not supported. Allowed types are: `auto`, `none`, `required`."
)
@@ -0,0 +1,14 @@
# Copyright (c) Microsoft. All rights reserved.
from enum import Enum
from semantic_kernel.utils.feature_stage_decorator import experimental
@experimental
class FunctionChoiceType(Enum):
"""The type of function choice behavior."""
AUTO = "auto"
NONE = "none"
REQUIRED = "required"
@@ -0,0 +1,56 @@
# Google - Gemini
Gemini models are Google's large language models. Semantic Kernel provides two connectors to access these models from Google Cloud.
## Google AI
You can access the Gemini API from Google AI Studio. This mode of access is for quick prototyping as it relies on API keys.
Follow [these instructions](https://cloud.google.com/docs/authentication/api-keys) to create an API key.
Once you have an API key, you can start using Gemini models in SK using the `google_ai` connector. Example:
```Python
kernel = Kernel()
kernel.add_service(
GoogleAIChatCompletion(
gemini_model_id="gemini-2.5-flash",
api_key="...",
)
)
...
```
> Alternatively, you can use an .env file to store the model id and api key.
## Vertex AI
Google also offers access to Gemini through its Vertex AI platform. Vertex AI provides a more complete solution to build your enterprise AI applications end-to-end. You can read more about it [here](https://cloud.google.com/vertex-ai/generative-ai/docs/migrate/migrate-google-ai).
This mode of access requires a Google Cloud service account. Follow these [instructions](https://cloud.google.com/vertex-ai/generative-ai/docs/migrate/migrate-google-ai) to create a Google Cloud project if you don't have one already. Remember the `project id` as it is required to access the models.
Follow the steps below to set up your environment to use the Vertex AI API:
- [Install the gcloud CLI](https://cloud.google.com/sdk/docs/install)
- [Initialize the gcloud CLI](https://cloud.google.com/sdk/docs/initializing)
Once you have your project and your environment is set up, you can start using Gemini models in SK using the `vertex_ai` connector. Example:
```Python
kernel = Kernel()
kernel.add_service(
GoogleAIChatCompletion(
project_id="...",
region="...",
gemini_model_id="gemini-2.5-flash",
use_vertexai=True,
)
)
...
```
> Alternatively, you can use an .env file to store the model id and project id.
## Why is there code that looks almost identical in the implementations on the two connectors
The two connectors have very similar implementations, including the utils files. However, they are fundamentally different as they depend on different packages from Google. Although the namings of many types are identical, they are different types.
@@ -0,0 +1,21 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.google.google_ai.google_ai_prompt_execution_settings import (
GoogleAIChatPromptExecutionSettings,
GoogleAIEmbeddingPromptExecutionSettings,
GoogleAIPromptExecutionSettings,
GoogleAITextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.google_ai.services.google_ai_chat_completion import GoogleAIChatCompletion
from semantic_kernel.connectors.ai.google.google_ai.services.google_ai_text_completion import GoogleAITextCompletion
from semantic_kernel.connectors.ai.google.google_ai.services.google_ai_text_embedding import GoogleAITextEmbedding
__all__ = [
"GoogleAIChatCompletion",
"GoogleAIChatPromptExecutionSettings",
"GoogleAIEmbeddingPromptExecutionSettings",
"GoogleAIPromptExecutionSettings",
"GoogleAITextCompletion",
"GoogleAITextEmbedding",
"GoogleAITextPromptExecutionSettings",
]
@@ -0,0 +1,21 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.google.google_ai.google_ai_prompt_execution_settings import (
GoogleAIChatPromptExecutionSettings,
GoogleAIEmbeddingPromptExecutionSettings,
GoogleAIPromptExecutionSettings,
GoogleAITextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.google_ai.services.google_ai_chat_completion import GoogleAIChatCompletion
from semantic_kernel.connectors.ai.google.google_ai.services.google_ai_text_completion import GoogleAITextCompletion
from semantic_kernel.connectors.ai.google.google_ai.services.google_ai_text_embedding import GoogleAITextEmbedding
__all__ = [
"GoogleAIChatCompletion",
"GoogleAIChatPromptExecutionSettings",
"GoogleAIEmbeddingPromptExecutionSettings",
"GoogleAIPromptExecutionSettings",
"GoogleAITextCompletion",
"GoogleAITextEmbedding",
"GoogleAITextPromptExecutionSettings",
]
@@ -0,0 +1,51 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Annotated, Any, Literal
from pydantic import Field
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
class GoogleAIPromptExecutionSettings(PromptExecutionSettings):
"""Google AI Prompt Execution Settings."""
stop_sequences: Annotated[list[str] | None, Field(max_length=5)] = None
response_mime_type: Literal["text/plain", "application/json"] | None = None
response_schema: Any | None = None
candidate_count: Annotated[int | None, Field(ge=1)] = None
max_output_tokens: Annotated[int | None, Field(ge=1)] = None
temperature: Annotated[float | None, Field(ge=0.0, le=2.0)] = None
top_p: float | None = None
top_k: int | None = None
class GoogleAITextPromptExecutionSettings(GoogleAIPromptExecutionSettings):
"""Google AI Text Prompt Execution Settings."""
pass
class GoogleAIChatPromptExecutionSettings(GoogleAIPromptExecutionSettings):
"""Google AI Chat Prompt Execution Settings."""
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_config: Annotated[
dict[str, Any] | None,
Field(
description="Do not set this manually. It is set by the service based "
"on the function choice configuration.",
),
] = None
class GoogleAIEmbeddingPromptExecutionSettings(PromptExecutionSettings):
"""Google AI Embedding Prompt Execution Settings."""
output_dimensionality: Annotated[int | None, Field(le=768)] = None
@@ -0,0 +1,46 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import ClassVar
from pydantic import SecretStr
from semantic_kernel.kernel_pydantic import KernelBaseSettings
class GoogleAISettings(KernelBaseSettings):
"""Google AI settings.
The settings are first loaded from environment variables with
the prefix 'GOOGLE_AI_'.
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 'GOOGLE_AI_' are:
- gemini_model_id: str - The Gemini model ID for the Google AI service, i.e. gemini-1.5-pro
This value can be found in the Google AI service deployment.
(Env var GOOGLE_AI_GEMINI_MODEL_ID)
- embedding_model_id: str - The embedding model ID for the Google AI service, i.e. text-embedding-004
This value can be found in the Google AI service deployment.
(Env var GOOGLE_AI_EMBEDDING_MODEL_ID)
- api_key: SecretStr - The API key for the Google AI service deployment.
This value can be found in the Google AI service deployment.
(Env var GOOGLE_AI_API_KEY)
- cloud_project_id: str - The Google Cloud project ID.
(Env var GOOGLE_AI_CLOUD_PROJECT_ID)
- cloud_region: str - The Google Cloud region.
(Env var GOOGLE_AI_CLOUD_REGION)
- use_vertexai: bool - Whether to use Vertex AI. If true, cloud_project_id and cloud_region must be provided.
(Env var GOOGLE_AI_USE_VERTEXAI)
"""
env_prefix: ClassVar[str] = "GOOGLE_AI_"
gemini_model_id: str | None = None
embedding_model_id: str | None = None
api_key: SecretStr | None = None
cloud_project_id: str | None = None
cloud_region: str | None = None
use_vertexai: bool = False
@@ -0,0 +1,19 @@
# Copyright (c) Microsoft. All rights reserved.
from abc import ABC
from typing import ClassVar
from google.genai import Client
from semantic_kernel.connectors.ai.google.google_ai.google_ai_settings import GoogleAISettings
from semantic_kernel.kernel_pydantic import KernelBaseModel
class GoogleAIBase(KernelBaseModel, ABC):
"""Google AI Service."""
MODEL_PROVIDER_NAME: ClassVar[str] = "googleai"
service_settings: GoogleAISettings
client: Client | None = None
@@ -0,0 +1,429 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
import sys
from collections.abc import AsyncGenerator, Callable
from typing import TYPE_CHECKING, Any, ClassVar
from google.genai import Client
from google.genai.types import Candidate, Content, GenerateContentConfigDict, GenerateContentResponse
from pydantic import ValidationError
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_choice_behavior import FunctionChoiceType
from semantic_kernel.connectors.ai.google.google_ai.google_ai_prompt_execution_settings import (
GoogleAIChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.google_ai.google_ai_settings import GoogleAISettings
from semantic_kernel.connectors.ai.google.google_ai.services.google_ai_base import GoogleAIBase
from semantic_kernel.connectors.ai.google.google_ai.services.utils import (
finish_reason_from_google_ai_to_semantic_kernel,
format_assistant_message,
format_tool_message,
format_user_message,
update_settings_from_function_choice_configuration,
)
from semantic_kernel.connectors.ai.google.shared_utils import (
collapse_function_call_results_in_chat_history,
filter_system_message,
format_gemini_function_name_to_kernel_function_fully_qualified_name,
)
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 (
ServiceInitializationError,
ServiceInvalidExecutionSettingsError,
)
from semantic_kernel.utils.telemetry.model_diagnostics.decorators import (
trace_chat_completion,
trace_streaming_chat_completion,
)
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
logger: logging.Logger = logging.getLogger(__name__)
class GoogleAIChatCompletion(GoogleAIBase, ChatCompletionClientBase):
"""Google AI Chat Completion Client."""
SUPPORTS_FUNCTION_CALLING: ClassVar[bool] = True
def __init__(
self,
gemini_model_id: str | None = None,
api_key: str | None = None,
project_id: str | None = None,
region: str | None = None,
use_vertexai: bool | None = None,
service_id: str | None = None,
client: Client | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize the Google AI Chat Completion Client.
If no arguments are provided, the service will attempt to load the settings from the environment.
The following environment variables are used:
- GOOGLE_AI_GEMINI_MODEL_ID
- GOOGLE_AI_API_KEY
- GOOGLE_AI_CLOUD_PROJECT_ID
- GOOGLE_AI_CLOUD_REGION
- GOOGLE_AI_USE_VERTEXAI
Args:
gemini_model_id (str | None): The Gemini model ID. (Optional)
api_key (str | None): The API key. (Optional)
project_id (str | None): The Google Cloud project ID. (Optional)
region (str | None): The Google Cloud region. (Optional)
use_vertexai (bool | None): Whether to use Vertex AI. (Optional)
service_id (str | None): The service ID. (Optional)
client (Client | None): The Google AI client to use for break glass scenarios. (Optional)
env_file_path (str | None): The path to the .env file. (Optional)
env_file_encoding (str | None): The encoding of the .env file. (Optional)
Raises:
ServiceInitializationError: If an error occurs during initialization.
"""
try:
google_ai_settings = GoogleAISettings(
gemini_model_id=gemini_model_id,
api_key=api_key,
cloud_project_id=project_id,
cloud_region=region,
use_vertexai=use_vertexai,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as e:
raise ServiceInitializationError(f"Failed to validate Google AI settings: {e}") from e
if not google_ai_settings.gemini_model_id:
raise ServiceInitializationError("The Google AI Gemini model ID is required.")
if not client:
if google_ai_settings.use_vertexai and not google_ai_settings.cloud_project_id:
raise ServiceInitializationError("Project ID must be provided when use_vertexai is True.")
if google_ai_settings.use_vertexai and not google_ai_settings.cloud_region:
raise ServiceInitializationError("Region must be provided when use_vertexai is True.")
if not google_ai_settings.use_vertexai and not google_ai_settings.api_key:
raise ServiceInitializationError("The API key is required when use_vertexai is False.")
super().__init__(
ai_model_id=google_ai_settings.gemini_model_id,
service_id=service_id or google_ai_settings.gemini_model_id,
service_settings=google_ai_settings,
client=client,
)
# region Overriding base class methods
# Override from AIServiceClientBase
@override
def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]:
return GoogleAIChatPromptExecutionSettings
@override
@trace_chat_completion(GoogleAIBase.MODEL_PROVIDER_NAME)
async def _inner_get_chat_message_contents(
self,
chat_history: "ChatHistory",
settings: "PromptExecutionSettings",
) -> list["ChatMessageContent"]:
if not isinstance(settings, GoogleAIChatPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, GoogleAIChatPromptExecutionSettings) # nosec
if not self.service_settings.gemini_model_id:
raise ServiceInitializationError("The Google AI Gemini model ID is required.")
collapse_function_call_results_in_chat_history(chat_history)
async def _generate_content(client: Client) -> GenerateContentResponse:
return await client.aio.models.generate_content(
model=self.service_settings.gemini_model_id, # type: ignore[arg-type]
contents=self._prepare_chat_history_for_request(chat_history), # type: ignore[arg-type]
config=GenerateContentConfigDict(
system_instruction=filter_system_message(chat_history),
**settings.prepare_settings_dict(), # type: ignore[typeddict-item]
),
)
if self.client:
response: GenerateContentResponse = await _generate_content(self.client)
elif self.service_settings.use_vertexai:
with Client(
vertexai=True,
project=self.service_settings.cloud_project_id,
location=self.service_settings.cloud_region,
) as client:
response: GenerateContentResponse = await _generate_content(client) # type: ignore[no-redef]
else:
with Client(api_key=self.service_settings.api_key.get_secret_value()) as client: # type: ignore[union-attr]
response: GenerateContentResponse = await _generate_content(client) # type: ignore[no-redef]
return [self._create_chat_message_content(response, candidate) for candidate in response.candidates] # type: ignore
@override
@trace_streaming_chat_completion(GoogleAIBase.MODEL_PROVIDER_NAME)
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, GoogleAIChatPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, GoogleAIChatPromptExecutionSettings) # nosec
if not self.service_settings.gemini_model_id:
raise ServiceInitializationError("The Google AI Gemini model ID is required.")
collapse_function_call_results_in_chat_history(chat_history)
async def _generate_content_stream(client: Client) -> AsyncGenerator[GenerateContentResponse, Any]:
async for chunk in await client.aio.models.generate_content_stream(
model=self.service_settings.gemini_model_id, # type: ignore[arg-type]
contents=self._prepare_chat_history_for_request(chat_history), # type: ignore[arg-type]
config=GenerateContentConfigDict(
system_instruction=filter_system_message(chat_history),
**settings.prepare_settings_dict(), # type: ignore[typeddict-item]
),
):
yield chunk
if self.client:
async for chunk in _generate_content_stream(self.client):
yield [
self._create_streaming_chat_message_content(chunk, candidate, function_invoke_attempt)
for candidate in chunk.candidates # type: ignore
]
elif self.service_settings.use_vertexai:
with Client(
vertexai=True,
project=self.service_settings.cloud_project_id,
location=self.service_settings.cloud_region,
) as client:
async for chunk in _generate_content_stream(client):
yield [
self._create_streaming_chat_message_content(chunk, candidate, function_invoke_attempt)
for candidate in chunk.candidates # type: ignore
]
else:
with Client(api_key=self.service_settings.api_key.get_secret_value()) as client: # type: ignore[union-attr]
async for chunk in _generate_content_stream(client):
yield [
self._create_streaming_chat_message_content(chunk, candidate, function_invoke_attempt)
for candidate in chunk.candidates # type: ignore
]
@override
def _verify_function_choice_settings(self, settings: "PromptExecutionSettings") -> None:
if not isinstance(settings, GoogleAIChatPromptExecutionSettings):
raise ServiceInvalidExecutionSettingsError("The settings must be an GoogleAIChatPromptExecutionSettings.")
if settings.candidate_count is not None and settings.candidate_count > 1:
raise ServiceInvalidExecutionSettingsError(
"Auto-invocation of tool calls may only be used with a "
"GoogleAIChatPromptExecutionSettings.candidate_count of 1."
)
@override
def _update_function_choice_settings_callback(
self,
) -> Callable[["FunctionCallChoiceConfiguration", "PromptExecutionSettings", FunctionChoiceType], None]:
return update_settings_from_function_choice_configuration
@override
def _reset_function_choice_settings(self, settings: "PromptExecutionSettings") -> None:
if hasattr(settings, "tool_config"):
settings.tool_config = 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[Content]:
chat_request_messages: list[Content] = []
for message in chat_history.messages:
if message.role == AuthorRole.SYSTEM:
# Skip system messages since they are not part of the chat request.
# System message will be provided as system_instruction in the config.
continue
if message.role == AuthorRole.USER:
chat_request_messages.append(Content(role="user", parts=format_user_message(message)))
elif message.role == AuthorRole.ASSISTANT:
chat_request_messages.append(Content(role="model", parts=format_assistant_message(message)))
elif message.role == AuthorRole.TOOL:
chat_request_messages.append(Content(role="function", parts=format_tool_message(message)))
return chat_request_messages
# endregion
# region Non-streaming
def _create_chat_message_content(
self, response: GenerateContentResponse, candidate: Candidate
) -> ChatMessageContent:
"""Create a chat message content object.
Args:
response: The response from the service.
candidate: The candidate from the response.
Returns:
A chat message content object.
"""
# Best effort conversion of finish reason. The raw value will be available in metadata.
finish_reason: FinishReason | None = finish_reason_from_google_ai_to_semantic_kernel(candidate.finish_reason)
response_metadata = self._get_metadata_from_response(response)
response_metadata.update(self._get_metadata_from_candidate(candidate))
items: list[CMC_ITEM_TYPES] = []
if candidate.content and candidate.content.parts:
for idx, part in enumerate(candidate.content.parts):
if part.text:
items.append(TextContent(text=part.text, inner_content=response, metadata=response_metadata))
elif part.function_call:
fc_metadata: dict[str, Any] = {}
thought_sig = getattr(part, "thought_signature", None)
if thought_sig:
fc_metadata["thought_signature"] = thought_sig
items.append(
FunctionCallContent(
id=f"{part.function_call.name}_{idx!s}",
name=format_gemini_function_name_to_kernel_function_fully_qualified_name(
part.function_call.name # type: ignore[arg-type]
),
arguments={k: v for k, v in part.function_call.args.items()}, # type: ignore
metadata=fc_metadata if fc_metadata else None,
)
)
return ChatMessageContent(
ai_model_id=self.ai_model_id,
role=AuthorRole.ASSISTANT,
items=items,
inner_content=response,
finish_reason=finish_reason,
metadata=response_metadata,
)
# endregion
# region Streaming
def _create_streaming_chat_message_content(
self,
chunk: GenerateContentResponse,
candidate: Candidate,
function_invoke_attempt: int = 0,
) -> StreamingChatMessageContent:
"""Create a streaming chat message content object.
Args:
chunk: The response from the service.
candidate: The candidate from the response.
function_invoke_attempt: The function invoke attempt.
Returns:
A streaming chat message content object.
"""
# Best effort conversion of finish reason. The raw value will be available in metadata.
finish_reason: FinishReason | None = finish_reason_from_google_ai_to_semantic_kernel(candidate.finish_reason)
response_metadata = self._get_metadata_from_response(chunk)
response_metadata.update(self._get_metadata_from_candidate(candidate))
items: list[STREAMING_ITEM_TYPES] = []
if candidate.content and candidate.content.parts:
for idx, part in enumerate(candidate.content.parts):
if part.text:
items.append(
StreamingTextContent(
choice_index=candidate.index or 0,
text=part.text,
inner_content=chunk,
metadata=response_metadata,
)
)
elif part.function_call:
fc_metadata: dict[str, Any] = {}
thought_sig = getattr(part, "thought_signature", None)
if thought_sig:
fc_metadata["thought_signature"] = thought_sig
items.append(
FunctionCallContent(
id=f"{part.function_call.name}_{idx!s}",
name=format_gemini_function_name_to_kernel_function_fully_qualified_name(
part.function_call.name # type: ignore[arg-type]
),
arguments={k: v for k, v in part.function_call.args.items()}, # type: ignore
metadata=fc_metadata if fc_metadata else None,
)
)
return StreamingChatMessageContent(
ai_model_id=self.ai_model_id,
role=AuthorRole.ASSISTANT,
choice_index=candidate.index or 0,
items=items,
inner_content=chunk,
finish_reason=finish_reason,
metadata=response_metadata,
function_invoke_attempt=function_invoke_attempt,
)
# endregion
def _get_metadata_from_response(self, response: GenerateContentResponse) -> dict[str, Any]:
"""Get metadata from the response.
Args:
response: The response from the service.
Returns:
A dictionary containing metadata.
"""
return {
"prompt_feedback": response.prompt_feedback,
"usage": CompletionUsage(
prompt_tokens=response.usage_metadata.prompt_token_count if response.usage_metadata else None,
completion_tokens=response.usage_metadata.candidates_token_count if response.usage_metadata else None,
),
}
def _get_metadata_from_candidate(self, candidate: Candidate) -> dict[str, Any]:
"""Get metadata from the candidate.
Args:
candidate: The candidate from the response.
Returns:
A dictionary containing metadata.
"""
return {
"index": candidate.index,
"finish_reason": candidate.finish_reason,
"safety_ratings": candidate.safety_ratings,
"token_count": candidate.token_count,
}
@@ -0,0 +1,262 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from collections.abc import AsyncGenerator
from typing import TYPE_CHECKING, Any
from google.genai import Client
from google.genai.types import Candidate, GenerateContentConfigDict, GenerateContentResponse
from pydantic import ValidationError
from semantic_kernel.connectors.ai.completion_usage import CompletionUsage
from semantic_kernel.connectors.ai.google.google_ai.google_ai_prompt_execution_settings import (
GoogleAITextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.google_ai.google_ai_settings import GoogleAISettings
from semantic_kernel.connectors.ai.google.google_ai.services.google_ai_base import GoogleAIBase
from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase
from semantic_kernel.contents import TextContent
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
from semantic_kernel.utils.telemetry.model_diagnostics.decorators import (
trace_streaming_text_completion,
trace_text_completion,
)
if TYPE_CHECKING:
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
class GoogleAITextCompletion(GoogleAIBase, TextCompletionClientBase):
"""Google AI Text Completion Client."""
def __init__(
self,
gemini_model_id: str | None = None,
api_key: str | None = None,
project_id: str | None = None,
region: str | None = None,
use_vertexai: bool | None = None,
service_id: str | None = None,
client: Client | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize the Google AI Text Completion Client.
If no arguments are provided, the service will attempt to load the settings from the environment.
The following environment variables are used:
- GOOGLE_AI_GEMINI_MODEL_ID
- GOOGLE_AI_API_KEY
- GOOGLE_AI_CLOUD_PROJECT_ID
- GOOGLE_AI_CLOUD_REGION
- GOOGLE_AI_USE_VERTEXAI
Args:
gemini_model_id (str | None): The Gemini model ID. (Optional)
api_key (str | None): The API key. (Optional)
project_id (str | None): The Google Cloud project ID. (Optional)
region (str | None): The Google Cloud region. (Optional)
use_vertexai (bool | None): Whether to use Vertex AI. (Optional)
service_id (str | None): The service ID. (Optional)
client (Client | None): The Google AI Client to use for break glass scenarios. (Optional)
env_file_path (str | None): The path to the .env file. (Optional)
env_file_encoding (str | None): The encoding of the .env file. (Optional)
Raises:
ServiceInitializationError: If an error occurs during initialization.
"""
try:
google_ai_settings = GoogleAISettings(
gemini_model_id=gemini_model_id,
api_key=api_key,
cloud_project_id=project_id,
cloud_region=region,
use_vertexai=use_vertexai,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as e:
raise ServiceInitializationError(f"Failed to validate Google AI settings: {e}") from e
if not google_ai_settings.gemini_model_id:
raise ServiceInitializationError("The Google AI Gemini model ID is required.")
if not client:
if google_ai_settings.use_vertexai and not google_ai_settings.cloud_project_id:
raise ServiceInitializationError("Project ID must be provided when use_vertexai is True.")
if google_ai_settings.use_vertexai and not google_ai_settings.cloud_region:
raise ServiceInitializationError("Region must be provided when use_vertexai is True.")
if not google_ai_settings.use_vertexai and not google_ai_settings.api_key:
raise ServiceInitializationError("The API key is required when use_vertexai is False.")
super().__init__(
ai_model_id=google_ai_settings.gemini_model_id,
service_id=service_id or google_ai_settings.gemini_model_id,
service_settings=google_ai_settings,
client=client,
)
# region Overriding base class methods
# Override from AIServiceClientBase
@override
def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]:
return GoogleAITextPromptExecutionSettings
@override
@trace_text_completion(GoogleAIBase.MODEL_PROVIDER_NAME)
async def _inner_get_text_contents(
self,
prompt: str,
settings: "PromptExecutionSettings",
) -> list[TextContent]:
if not isinstance(settings, GoogleAITextPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, GoogleAITextPromptExecutionSettings) # nosec
if not self.service_settings.gemini_model_id:
raise ServiceInitializationError("The Google AI Gemini model ID is required.")
async def _generate_content(client: Client) -> GenerateContentResponse:
return await client.aio.models.generate_content(
model=self.service_settings.gemini_model_id, # type: ignore[arg-type]
contents=prompt,
config=GenerateContentConfigDict(**settings.prepare_settings_dict()), # type: ignore[typeddict-item]
)
if self.client:
response: GenerateContentResponse = await _generate_content(self.client)
elif self.service_settings.use_vertexai:
with Client(
vertexai=True,
project=self.service_settings.cloud_project_id,
location=self.service_settings.cloud_region,
) as client:
response: GenerateContentResponse = await _generate_content(client) # type: ignore[no-redef]
else:
with Client(api_key=self.service_settings.api_key.get_secret_value()) as client: # type: ignore[union-attr]
response: GenerateContentResponse = await _generate_content(client) # type: ignore[no-redef]
return [self._create_text_content(response, candidate) for candidate in response.candidates] # type: ignore
@override
@trace_streaming_text_completion(GoogleAIBase.MODEL_PROVIDER_NAME)
async def _inner_get_streaming_text_contents(
self,
prompt: str,
settings: "PromptExecutionSettings",
) -> AsyncGenerator[list[StreamingTextContent], Any]:
if not isinstance(settings, GoogleAITextPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, GoogleAITextPromptExecutionSettings) # nosec
if not self.service_settings.gemini_model_id:
raise ServiceInitializationError("The Google AI Gemini model ID is required.")
async def _generate_content_stream(client: Client) -> AsyncGenerator[GenerateContentResponse, Any]:
async for chunk in await client.aio.models.generate_content_stream(
model=self.service_settings.gemini_model_id, # type: ignore[arg-type]
contents=prompt,
config=GenerateContentConfigDict(**settings.prepare_settings_dict()), # type: ignore[typeddict-item]
):
yield chunk
if self.client:
async for chunk in _generate_content_stream(self.client):
yield [self._create_streaming_text_content(chunk, candidate) for candidate in chunk.candidates] # type: ignore
elif self.service_settings.use_vertexai:
with Client(
vertexai=True,
project=self.service_settings.cloud_project_id,
location=self.service_settings.cloud_region,
) as client:
async for chunk in _generate_content_stream(client):
yield [self._create_streaming_text_content(chunk, candidate) for candidate in chunk.candidates] # type: ignore
else:
with Client(api_key=self.service_settings.api_key.get_secret_value()) as client: # type: ignore[union-attr]
async for chunk in _generate_content_stream(client):
yield [self._create_streaming_text_content(chunk, candidate) for candidate in chunk.candidates] # type: ignore
# endregion
def _create_text_content(self, response: GenerateContentResponse, candidate: Candidate) -> TextContent:
"""Create a text content object.
Args:
response: The response from the service.
candidate: The candidate from the response.
Returns:
A text content object.
"""
response_metadata = self._get_metadata_from_response(response)
response_metadata.update(self._get_metadata_from_candidate(candidate))
return TextContent(
ai_model_id=self.ai_model_id,
text=candidate.content.parts[0].text or "" if candidate.content and candidate.content.parts else "",
inner_content=response,
metadata=response_metadata,
)
def _create_streaming_text_content(
self, chunk: GenerateContentResponse, candidate: Candidate
) -> StreamingTextContent:
"""Create a streaming text content object.
Args:
chunk: The response from the service.
candidate: The candidate from the response.
Returns:
A streaming text content object.
"""
response_metadata = self._get_metadata_from_response(chunk)
response_metadata.update(self._get_metadata_from_candidate(candidate))
return StreamingTextContent(
ai_model_id=self.ai_model_id,
choice_index=candidate.index or 0,
text=candidate.content.parts[0].text or "" if candidate.content and candidate.content.parts else "",
inner_content=chunk,
metadata=response_metadata,
)
def _get_metadata_from_response(self, response: GenerateContentResponse) -> dict[str, Any]:
"""Get metadata from the response.
Args:
response: The response from the service.
Returns:
A dictionary containing metadata.
"""
return {
"prompt_feedback": response.prompt_feedback,
"usage": CompletionUsage(
prompt_tokens=response.usage_metadata.prompt_token_count if response.usage_metadata else None,
completion_tokens=response.usage_metadata.candidates_token_count if response.usage_metadata else None,
),
}
def _get_metadata_from_candidate(self, candidate: Candidate) -> dict[str, Any]:
"""Get metadata from the candidate.
Args:
candidate: The candidate from the response.
Returns:
A dictionary containing metadata.
"""
return {
"index": candidate.index,
"finish_reason": candidate.finish_reason,
"safety_ratings": candidate.safety_ratings,
"token_count": candidate.token_count,
}
@@ -0,0 +1,149 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from typing import Any
from google.genai import Client
from google.genai.types import EmbedContentConfigDict, EmbedContentResponse
from numpy import array, ndarray
from pydantic import ValidationError
from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase
from semantic_kernel.connectors.ai.google.google_ai.google_ai_prompt_execution_settings import (
GoogleAIEmbeddingPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.google_ai.google_ai_settings import GoogleAISettings
from semantic_kernel.connectors.ai.google.google_ai.services.google_ai_base import GoogleAIBase
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
class GoogleAITextEmbedding(GoogleAIBase, EmbeddingGeneratorBase):
"""Google AI Text Embedding Service."""
def __init__(
self,
embedding_model_id: str | None = None,
api_key: str | None = None,
project_id: str | None = None,
region: str | None = None,
use_vertexai: bool | None = None,
service_id: str | None = None,
client: Client | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize the Google AI 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:
- GOOGLE_AI_EMBEDDING_MODEL_ID
- GOOGLE_AI_API_KEY
- GOOGLE_AI_CLOUD_PROJECT_ID
- GOOGLE_AI_CLOUD_REGION
- GOOGLE_AI_USE_VERTEXAI
Args:
embedding_model_id (str | None): The embedding model ID. (Optional)
api_key (str | None): The API key. (Optional)
project_id (str | None): The Google Cloud project ID. (Optional)
region (str | None): The Google Cloud region. (Optional)
use_vertexai (bool | None): Whether to use Vertex AI. (Optional)
service_id (str | None): The service ID. (Optional)
client (Client | None): The Google AI Client to use for break glass scenarios. (Optional)
env_file_path (str | None): The path to the .env file. (Optional)
env_file_encoding (str | None): The encoding of the .env file. (Optional)
Raises:
ServiceInitializationError: If an error occurs during initialization.
"""
try:
google_ai_settings = GoogleAISettings(
embedding_model_id=embedding_model_id,
api_key=api_key,
cloud_project_id=project_id,
cloud_region=region,
use_vertexai=use_vertexai,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as e:
raise ServiceInitializationError(f"Failed to validate Google AI settings: {e}") from e
if not google_ai_settings.embedding_model_id:
raise ServiceInitializationError("The Google AI embedding model ID is required.")
if not client:
if google_ai_settings.use_vertexai and not google_ai_settings.cloud_project_id:
raise ServiceInitializationError("Project ID must be provided when use_vertexai is True.")
if google_ai_settings.use_vertexai and not google_ai_settings.cloud_region:
raise ServiceInitializationError("Region must be provided when use_vertexai is True.")
if not google_ai_settings.use_vertexai and not google_ai_settings.api_key:
raise ServiceInitializationError("The API key is required when use_vertexai is False.")
super().__init__(
ai_model_id=google_ai_settings.embedding_model_id,
service_id=service_id or google_ai_settings.embedding_model_id,
service_settings=google_ai_settings,
client=client,
)
@override
async def generate_embeddings(
self,
texts: list[str],
settings: "PromptExecutionSettings | None" = None,
**kwargs: Any,
) -> ndarray:
raw_embeddings = await self.generate_raw_embeddings(texts, settings, **kwargs)
return array(raw_embeddings)
@override
async def generate_raw_embeddings(
self,
texts: list[str],
settings: "PromptExecutionSettings | None" = None,
**kwargs: Any,
) -> list[list[float]]:
if not settings:
settings = GoogleAIEmbeddingPromptExecutionSettings()
else:
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, GoogleAIEmbeddingPromptExecutionSettings) # nosec
if not self.service_settings.embedding_model_id:
raise ServiceInitializationError("The Google AI embedding model ID is required.")
async def _embed_content(client: Client) -> EmbedContentResponse:
return await client.aio.models.embed_content(
model=self.service_settings.embedding_model_id, # type: ignore[arg-type]
contents=texts, # type: ignore[arg-type]
config=EmbedContentConfigDict(output_dimensionality=settings.output_dimensionality),
)
if self.client:
response: EmbedContentResponse = await _embed_content(self.client)
elif self.service_settings.use_vertexai:
with Client(
vertexai=True,
project=self.service_settings.cloud_project_id,
location=self.service_settings.cloud_region,
) as client:
response: EmbedContentResponse = await _embed_content(client) # type: ignore[no-redef]
else:
with Client(api_key=self.service_settings.api_key.get_secret_value()) as client: # type: ignore[union-attr]
response: EmbedContentResponse = await _embed_content(client) # type: ignore[no-redef]
return [embedding.values for embedding in response.embeddings] # type: ignore
@override
def get_prompt_execution_settings_class(
self,
) -> type["PromptExecutionSettings"]:
"""Get the request settings class."""
return GoogleAIEmbeddingPromptExecutionSettings
@@ -0,0 +1,203 @@
# Copyright (c) Microsoft. All rights reserved.
import json
import logging
from typing import TYPE_CHECKING, Any
from google.genai.types import FinishReason, Part
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType
from semantic_kernel.connectors.ai.google.google_ai.google_ai_prompt_execution_settings import (
GoogleAIChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.shared_utils import (
FUNCTION_CHOICE_TYPE_TO_GOOGLE_FUNCTION_CALLING_MODE,
GEMINI_FUNCTION_NAME_SEPARATOR,
sanitize_schema_for_google_ai,
)
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.finish_reason import FinishReason as SemanticKernelFinishReason
from semantic_kernel.exceptions.service_exceptions import ServiceInvalidRequestError
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
logger: logging.Logger = logging.getLogger(__name__)
def finish_reason_from_google_ai_to_semantic_kernel(
finish_reason: FinishReason | None,
) -> SemanticKernelFinishReason | None:
"""Convert a Google AI FinishReason to a Semantic Kernel FinishReason.
This is best effort and may not cover all cases as the enums are not identical.
"""
if finish_reason is None:
return None
if finish_reason == FinishReason.STOP:
return SemanticKernelFinishReason.STOP
if finish_reason == FinishReason.MAX_TOKENS:
return SemanticKernelFinishReason.LENGTH
if finish_reason == FinishReason.SAFETY:
return SemanticKernelFinishReason.CONTENT_FILTER
return None
def format_user_message(message: ChatMessageContent) -> list[Part]:
"""Format a user message to the expected object for the client.
Args:
message: The user message.
Returns:
The formatted user message as a list of parts.
"""
parts: list[Part] = []
for item in message.items:
if isinstance(item, TextContent):
parts.append(Part.from_text(text=item.text))
elif isinstance(item, ImageContent):
parts.append(_create_image_part(item))
else:
raise ServiceInvalidRequestError(
"Unsupported item type in User message while formatting chat history for Google AI"
f" Inference: {type(item)}"
)
return parts
def format_assistant_message(message: ChatMessageContent) -> list[Part]:
"""Format an assistant message to the expected object for the client.
Args:
message: The assistant message.
Returns:
The formatted assistant message as a list of parts.
"""
parts: list[Part] = []
for item in message.items:
if isinstance(item, TextContent):
if item.text:
parts.append(Part.from_text(text=item.text))
elif isinstance(item, FunctionCallContent):
thought_signature = item.metadata.get("thought_signature") if item.metadata else None
if thought_signature:
parts.append(
Part(
function_call={
"name": item.name, # type: ignore[arg-type]
"args": json.loads(item.arguments) if isinstance(item.arguments, str) else item.arguments,
},
thought_signature=thought_signature,
)
)
else:
parts.append(
Part.from_function_call(
name=item.name, # type: ignore[arg-type]
args=json.loads(item.arguments) if isinstance(item.arguments, str) else item.arguments, # type: ignore[arg-type]
)
)
elif isinstance(item, ImageContent):
parts.append(_create_image_part(item))
else:
raise ServiceInvalidRequestError(
"Unsupported item type in Assistant message while formatting chat history for Google AI"
f" Inference: {type(item)}"
)
return parts
def format_tool_message(message: ChatMessageContent) -> list[Part]:
"""Format a tool message to the expected object for the client.
Args:
message: The tool message.
Returns:
The formatted tool message.
"""
parts: list[Part] = []
for item in message.items:
if isinstance(item, FunctionResultContent):
gemini_function_name = item.custom_fully_qualified_name(GEMINI_FUNCTION_NAME_SEPARATOR)
parts.append(
Part.from_function_response(
name=gemini_function_name,
response={
"content": str(item.result),
},
)
)
return parts
def kernel_function_metadata_to_google_ai_function_call_format(metadata: KernelFunctionMetadata) -> dict[str, Any]:
"""Convert the kernel function metadata to function calling format."""
parameters: dict[str, Any] | None = None
if metadata.parameters:
properties = {}
for param in metadata.parameters:
if param.name is None:
continue
prop_schema = sanitize_schema_for_google_ai(param.schema_data) if param.schema_data else param.schema_data
properties[param.name] = prop_schema
parameters = {
"type": "object",
"properties": properties,
"required": [p.name for p in metadata.parameters if p.is_required and p.name is not None],
}
return {
"name": metadata.custom_fully_qualified_name(GEMINI_FUNCTION_NAME_SEPARATOR),
"description": metadata.description or "",
"parameters": parameters,
}
def update_settings_from_function_choice_configuration(
function_choice_configuration: "FunctionCallChoiceConfiguration",
settings: "PromptExecutionSettings",
type: FunctionChoiceType,
) -> None:
"""Update the settings from a FunctionChoiceConfiguration."""
assert isinstance(settings, GoogleAIChatPromptExecutionSettings) # nosec
if function_choice_configuration.available_functions:
settings.tool_config = {
"function_calling_config": {
"mode": FUNCTION_CHOICE_TYPE_TO_GOOGLE_FUNCTION_CALLING_MODE[type],
}
}
settings.tools = [
{
"function_declarations": [
kernel_function_metadata_to_google_ai_function_call_format(f)
for f in function_choice_configuration.available_functions
]
}
]
def _create_image_part(image_content: ImageContent) -> Part:
if image_content.data_uri:
return Part.from_bytes(data=image_content.data, mime_type=image_content.mime_type) # type: ignore[arg-type]
# The Google AI API doesn't support images from arbitrary URIs:
# https://github.com/google-gemini/generative-ai-python/issues/357
raise ServiceInvalidRequestError(
"ImageContent without data_uri in User message while formatting chat history for Google AI"
)
@@ -0,0 +1,135 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
from copy import deepcopy
from typing import Any
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType
from semantic_kernel.const import DEFAULT_FULLY_QUALIFIED_NAME_SEPARATOR
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.exceptions.service_exceptions import ServiceInvalidRequestError
logger: logging.Logger = logging.getLogger(__name__)
def filter_system_message(chat_history: ChatHistory) -> str | None:
"""Filter the first system message from the chat history.
If there are multiple system messages, raise an error.
If there are no system messages, return None.
"""
if len([message for message in chat_history if message.role == AuthorRole.SYSTEM]) > 1:
raise ServiceInvalidRequestError(
"Multiple system messages in chat history. Only one system message is expected."
)
for message in chat_history:
if message.role == AuthorRole.SYSTEM:
return message.content
return None
FUNCTION_CHOICE_TYPE_TO_GOOGLE_FUNCTION_CALLING_MODE = {
FunctionChoiceType.AUTO: "AUTO",
FunctionChoiceType.NONE: "NONE",
FunctionChoiceType.REQUIRED: "ANY",
}
# The separator used in the fully qualified name of the function instead of the default "-" separator.
# This is required since Gemini doesn't work well with "-" in the function name.
# https://ai.google.dev/gemini-api/docs/function-calling#function_declarations
# Using double underscore to avoid situations where the function name already contains a single underscore.
# For example, we may incorrect split a function name with a single score when the function doesn't have a plugin name.
GEMINI_FUNCTION_NAME_SEPARATOR = "__"
def format_gemini_function_name_to_kernel_function_fully_qualified_name(gemini_function_name: str) -> str:
"""Format the Gemini function name to the kernel function fully qualified name."""
if GEMINI_FUNCTION_NAME_SEPARATOR in gemini_function_name:
plugin_name, function_name = gemini_function_name.split(GEMINI_FUNCTION_NAME_SEPARATOR, 1)
return f"{plugin_name}{DEFAULT_FULLY_QUALIFIED_NAME_SEPARATOR}{function_name}"
return gemini_function_name
def sanitize_schema_for_google_ai(schema: dict[str, Any] | None) -> dict[str, Any] | None:
"""Sanitize a JSON schema dict so it is compatible with Google AI / Vertex AI.
The Google AI protobuf ``Schema`` does not support ``anyOf``, ``oneOf``, or
``allOf``. It also does not accept ``type`` as an array (e.g.
``["string", "null"]``). This helper recursively rewrites those constructs
into the subset that Google AI understands, using ``nullable`` where
appropriate.
"""
if schema is None:
return None
schema = deepcopy(schema)
return _sanitize_node(schema)
def _sanitize_node(node: dict[str, Any]) -> dict[str, Any]:
"""Recursively sanitize a single schema node."""
# --- handle ``type`` given as a list (e.g. ["string", "null"]) ---
type_val = node.get("type")
if isinstance(type_val, list):
non_null = [t for t in type_val if t != "null"]
if len(type_val) != len(non_null):
node["nullable"] = True
node["type"] = non_null[0] if non_null else "string"
# --- handle ``anyOf`` / ``oneOf`` / ``allOf`` ---
for key in ("anyOf", "oneOf", "allOf"):
variants = node.get(key)
if not variants:
continue
non_null = [v for v in variants if v.get("type") != "null"]
has_null = len(variants) != len(non_null)
chosen = _sanitize_node(non_null[0]) if non_null else {"type": "string"}
# Preserve description from the outer node
desc = node.get("description")
node.clear()
node.update(chosen)
if has_null:
node["nullable"] = True
if desc and "description" not in node:
node["description"] = desc
break # only process the first matching key
# --- recurse into nested structures ---
props = node.get("properties")
if isinstance(props, dict):
for prop_name, prop_schema in props.items():
if isinstance(prop_schema, dict):
props[prop_name] = _sanitize_node(prop_schema)
items = node.get("items")
if isinstance(items, dict):
node["items"] = _sanitize_node(items)
return node
def collapse_function_call_results_in_chat_history(chat_history: ChatHistory):
"""The Gemini API expects the results of parallel function calls to be contained in a single message to be returned.
This helper method collapses the results of parallel function calls in the chat history into a single Tool message.
Since this method in an internal method that is supposed to be called only by the Google AI and Vertex AI
connectors, it is safe to assume that the chat history contains a correct sequence of messages, i.e. there won't be
cases where the assistant wants to call 2 functions in parallel but there are more than 2 function results following
the assistant message.
"""
if not chat_history.messages:
return
current_idx = 1
while current_idx < len(chat_history):
previous_message = chat_history[current_idx - 1]
current_message = chat_history[current_idx]
if previous_message.role == AuthorRole.TOOL and current_message.role == AuthorRole.TOOL:
previous_message.items.extend(current_message.items)
chat_history.remove_message(current_message)
else:
current_idx += 1
@@ -0,0 +1,31 @@
# Copyright (c) Microsoft. All rights reserved.
import warnings
from semantic_kernel.connectors.ai.google.vertex_ai.services.vertex_ai_chat_completion import VertexAIChatCompletion
from semantic_kernel.connectors.ai.google.vertex_ai.services.vertex_ai_text_completion import VertexAITextCompletion
from semantic_kernel.connectors.ai.google.vertex_ai.services.vertex_ai_text_embedding import VertexAITextEmbedding
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_prompt_execution_settings import (
VertexAIChatPromptExecutionSettings,
VertexAIEmbeddingPromptExecutionSettings,
VertexAIPromptExecutionSettings,
VertexAITextPromptExecutionSettings,
)
# Deprecation warning for the entire Vertex AI package
warnings.warn(
"The `semantic_kernel.connectors.ai.google.vertex_ai` package is deprecated and will be removed after 01/01/2026. "
"Please use `semantic_kernel.connectors.ai.google` instead for Google AI services.",
DeprecationWarning,
stacklevel=2,
)
__all__ = [
"VertexAIChatCompletion",
"VertexAIChatPromptExecutionSettings",
"VertexAIEmbeddingPromptExecutionSettings",
"VertexAIPromptExecutionSettings",
"VertexAITextCompletion",
"VertexAITextEmbedding",
"VertexAITextPromptExecutionSettings",
]
@@ -0,0 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
import warnings
# Deprecation warning for Vertex AI services
warnings.warn(
"The semantic_kernel.connectors.ai.google.vertex_ai module is deprecated and will be removed after 01/01/2026. "
"Please use semantic_kernel.connectors.ai.google instead for Google AI services.",
DeprecationWarning,
stacklevel=2,
)
@@ -0,0 +1,191 @@
# Copyright (c) Microsoft. All rights reserved.
import json
import logging
from typing import TYPE_CHECKING, Any
from google.cloud.aiplatform_v1beta1.types.content import Candidate
from vertexai.generative_models import FunctionDeclaration, Part, Tool, ToolConfig
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType
from semantic_kernel.connectors.ai.google.shared_utils import (
FUNCTION_CHOICE_TYPE_TO_GOOGLE_FUNCTION_CALLING_MODE,
GEMINI_FUNCTION_NAME_SEPARATOR,
sanitize_schema_for_google_ai,
)
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_prompt_execution_settings import (
VertexAIChatPromptExecutionSettings,
)
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.finish_reason import FinishReason as SemanticKernelFinishReason
from semantic_kernel.exceptions.service_exceptions import ServiceInvalidRequestError
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
logger: logging.Logger = logging.getLogger(__name__)
def finish_reason_from_vertex_ai_to_semantic_kernel(
finish_reason: Candidate.FinishReason,
) -> SemanticKernelFinishReason | None:
"""Convert a Vertex AI FinishReason to a Semantic Kernel FinishReason.
This is best effort and may not cover all cases as the enums are not identical.
"""
if finish_reason == Candidate.FinishReason.STOP:
return SemanticKernelFinishReason.STOP
if finish_reason == Candidate.FinishReason.MAX_TOKENS:
return SemanticKernelFinishReason.LENGTH
if finish_reason == Candidate.FinishReason.SAFETY:
return SemanticKernelFinishReason.CONTENT_FILTER
return None
def format_user_message(message: ChatMessageContent) -> list[Part]:
"""Format a user message to the expected object for the client.
Args:
message: The user message.
Returns:
The formatted user message as a list of parts.
"""
parts: list[Part] = []
for item in message.items:
if isinstance(item, TextContent):
parts.append(Part.from_text(item.text))
elif isinstance(item, ImageContent):
parts.append(_create_image_part(item))
else:
raise ServiceInvalidRequestError(
"Unsupported item type in User message while formatting chat history for Vertex AI"
f" Inference: {type(item)}"
)
return parts
def format_assistant_message(message: ChatMessageContent) -> list[Part]:
"""Format an assistant message to the expected object for the client.
Args:
message: The assistant message.
Returns:
The formatted assistant message as a list of parts.
"""
parts: list[Part] = []
for item in message.items:
if isinstance(item, TextContent):
if item.text:
parts.append(Part.from_text(item.text))
elif isinstance(item, FunctionCallContent):
part_dict: dict[str, Any] = {
"function_call": {
"name": item.name, # type: ignore[arg-type]
"args": json.loads(item.arguments) if isinstance(item.arguments, str) else item.arguments,
}
}
thought_signature = item.metadata.get("thought_signature") if item.metadata else None
if thought_signature:
part_dict["thought_signature"] = thought_signature
parts.append(Part.from_dict(part_dict))
elif isinstance(item, ImageContent):
parts.append(_create_image_part(item))
else:
raise ServiceInvalidRequestError(
"Unsupported item type in Assistant message while formatting chat history for Vertex AI"
f" Inference: {type(item)}"
)
return parts
def format_tool_message(message: ChatMessageContent) -> list[Part]:
"""Format a tool message to the expected object for the client.
Args:
message: The tool message.
Returns:
The formatted tool message.
"""
parts: list[Part] = []
for item in message.items:
if isinstance(item, FunctionResultContent):
gemini_function_name = item.custom_fully_qualified_name(GEMINI_FUNCTION_NAME_SEPARATOR)
parts.append(
Part.from_function_response(
gemini_function_name,
{
"content": str(item.result),
},
)
)
return parts
def kernel_function_metadata_to_vertex_ai_function_call_format(metadata: KernelFunctionMetadata) -> FunctionDeclaration:
"""Convert the kernel function metadata to function calling format."""
properties: dict[str, Any] = {}
if metadata.parameters:
for param in metadata.parameters:
if param.name is None:
continue
prop_schema = sanitize_schema_for_google_ai(param.schema_data) if param.schema_data else param.schema_data
properties[param.name] = prop_schema
return FunctionDeclaration(
name=metadata.custom_fully_qualified_name(GEMINI_FUNCTION_NAME_SEPARATOR),
description=metadata.description or "",
parameters={
"type": "object",
"properties": properties,
"required": [p.name for p in metadata.parameters if p.is_required and p.name is not None],
},
)
def update_settings_from_function_choice_configuration(
function_choice_configuration: "FunctionCallChoiceConfiguration",
settings: "PromptExecutionSettings",
type: FunctionChoiceType,
) -> None:
"""Update the settings from a FunctionChoiceConfiguration."""
assert isinstance(settings, VertexAIChatPromptExecutionSettings) # nosec
if function_choice_configuration.available_functions:
settings.tool_config = ToolConfig(
function_calling_config=ToolConfig.FunctionCallingConfig(
mode=FUNCTION_CHOICE_TYPE_TO_GOOGLE_FUNCTION_CALLING_MODE[type],
),
)
settings.tools = [
Tool(
function_declarations=[
kernel_function_metadata_to_vertex_ai_function_call_format(f)
for f in function_choice_configuration.available_functions
]
)
]
def _create_image_part(image_content: ImageContent) -> Part:
if image_content.data_uri:
return Part.from_data(image_content.data, image_content.mime_type) # type: ignore[arg-type]
# The Google AI API doesn't support images from arbitrary URIs:
# https://github.com/google-gemini/generative-ai-python/issues/357
raise ServiceInvalidRequestError(
"ImageContent without data_uri in User message while formatting chat history for Google AI"
)
@@ -0,0 +1,18 @@
# Copyright (c) Microsoft. All rights reserved.
from abc import ABC
from typing import ClassVar
from typing_extensions import deprecated
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_settings import VertexAISettings
from semantic_kernel.kernel_pydantic import KernelBaseModel
@deprecated("VertexAIBase is deprecated and will be removed after 01/01/2026. Use google_ai connectors instead.")
class VertexAIBase(KernelBaseModel, ABC):
"""Vertex AI Service."""
MODEL_PROVIDER_NAME: ClassVar[str] = "vertexai"
service_settings: VertexAISettings
@@ -0,0 +1,375 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from collections.abc import AsyncGenerator, AsyncIterable, Callable
from typing import TYPE_CHECKING, Any, ClassVar
import vertexai
from pydantic import ValidationError
from typing_extensions import deprecated
from vertexai.generative_models import Candidate, Content, GenerationResponse, GenerativeModel
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_choice_behavior import FunctionChoiceType
from semantic_kernel.connectors.ai.google.shared_utils import (
collapse_function_call_results_in_chat_history,
filter_system_message,
format_gemini_function_name_to_kernel_function_fully_qualified_name,
)
from semantic_kernel.connectors.ai.google.vertex_ai.services.utils import (
finish_reason_from_vertex_ai_to_semantic_kernel,
format_assistant_message,
format_tool_message,
format_user_message,
update_settings_from_function_choice_configuration,
)
from semantic_kernel.connectors.ai.google.vertex_ai.services.vertex_ai_base import VertexAIBase
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_prompt_execution_settings import (
VertexAIChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_settings import VertexAISettings
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 (
ServiceInitializationError,
ServiceInvalidExecutionSettingsError,
)
from semantic_kernel.utils.telemetry.model_diagnostics.decorators import (
trace_chat_completion,
trace_streaming_chat_completion,
)
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
@deprecated(
"VertexAIChatCompletion is deprecated and will be removed after 01/01/2026. "
"Use `semantic_kernel.connectors.ai.google.GoogleAIChatCompletion` connectors instead."
)
class VertexAIChatCompletion(VertexAIBase, ChatCompletionClientBase):
"""Google Vertex AI Chat Completion Service."""
SUPPORTS_FUNCTION_CALLING: ClassVar[bool] = True
def __init__(
self,
project_id: str | None = None,
region: str | None = None,
gemini_model_id: str | None = None,
service_id: str | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize the Google Vertex AI 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:
- VERTEX_AI_GEMINI_MODEL_ID
- VERTEX_AI_PROJECT_ID
- VERTEX_AI_REGION
Args:
project_id (str): The Google Cloud project ID.
region (str): The Google Cloud region.
gemini_model_id (str): The Gemini model ID.
service_id (str): The Vertex AI service ID.
env_file_path (str): The path to the environment file.
env_file_encoding (str): The encoding of the environment file.
"""
try:
vertex_ai_settings = VertexAISettings(
project_id=project_id,
region=region,
gemini_model_id=gemini_model_id,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as e:
raise ServiceInitializationError(f"Failed to validate Vertex AI settings: {e}") from e
if not vertex_ai_settings.gemini_model_id:
raise ServiceInitializationError("The Vertex AI Gemini model ID is required.")
super().__init__(
ai_model_id=vertex_ai_settings.gemini_model_id,
service_id=service_id or vertex_ai_settings.gemini_model_id,
service_settings=vertex_ai_settings,
)
# region Overriding base class methods
# Override from AIServiceClientBase
@override
def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]:
return VertexAIChatPromptExecutionSettings
@override
@trace_chat_completion(VertexAIBase.MODEL_PROVIDER_NAME)
async def _inner_get_chat_message_contents(
self,
chat_history: "ChatHistory",
settings: "PromptExecutionSettings",
) -> list["ChatMessageContent"]:
if not isinstance(settings, VertexAIChatPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, VertexAIChatPromptExecutionSettings) # nosec
vertexai.init(project=self.service_settings.project_id, location=self.service_settings.region)
assert self.service_settings.gemini_model_id is not None # nosec
model = GenerativeModel(
self.service_settings.gemini_model_id,
system_instruction=filter_system_message(chat_history),
)
collapse_function_call_results_in_chat_history(chat_history)
response: GenerationResponse = await model.generate_content_async(
contents=self._prepare_chat_history_for_request(chat_history),
generation_config=settings.prepare_settings_dict(),
tools=settings.tools,
tool_config=settings.tool_config,
)
return [self._create_chat_message_content(response, candidate) for candidate in response.candidates]
@override
@trace_streaming_chat_completion(VertexAIBase.MODEL_PROVIDER_NAME)
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, VertexAIChatPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, VertexAIChatPromptExecutionSettings) # nosec
vertexai.init(project=self.service_settings.project_id, location=self.service_settings.region)
assert self.service_settings.gemini_model_id is not None # nosec
model = GenerativeModel(
self.service_settings.gemini_model_id,
system_instruction=filter_system_message(chat_history),
)
collapse_function_call_results_in_chat_history(chat_history)
response: AsyncIterable[GenerationResponse] = await model.generate_content_async(
contents=self._prepare_chat_history_for_request(chat_history),
generation_config=settings.prepare_settings_dict(),
tools=settings.tools,
tool_config=settings.tool_config,
stream=True,
)
async for chunk in response:
yield [
self._create_streaming_chat_message_content(chunk, candidate, function_invoke_attempt)
for candidate in chunk.candidates
]
@override
def _verify_function_choice_settings(self, settings: "PromptExecutionSettings") -> None:
if not isinstance(settings, VertexAIChatPromptExecutionSettings):
raise ServiceInvalidExecutionSettingsError("The settings must be an VertexAIChatPromptExecutionSettings.")
if settings.candidate_count is not None and settings.candidate_count > 1:
raise ServiceInvalidExecutionSettingsError(
"Auto-invocation of tool calls may only be used with a "
"VertexAIChatPromptExecutionSettings.candidate_count of 1."
)
@override
def _update_function_choice_settings_callback(
self,
) -> Callable[["FunctionCallChoiceConfiguration", "PromptExecutionSettings", FunctionChoiceType], None]:
return update_settings_from_function_choice_configuration
@override
def _reset_function_choice_settings(self, settings: "PromptExecutionSettings") -> None:
if hasattr(settings, "tool_config"):
settings.tool_config = 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[Content]:
chat_request_messages: list[Content] = []
for message in chat_history.messages:
if message.role == AuthorRole.SYSTEM:
# Skip system messages since they are not part of the chat request.
# System message will be provided as system_instruction in the model.
continue
if message.role == AuthorRole.USER:
chat_request_messages.append(Content(role="user", parts=format_user_message(message)))
elif message.role == AuthorRole.ASSISTANT:
chat_request_messages.append(Content(role="model", parts=format_assistant_message(message)))
elif message.role == AuthorRole.TOOL:
chat_request_messages.append(Content(role="function", parts=format_tool_message(message)))
return chat_request_messages
# endregion
# region Non-streaming
def _create_chat_message_content(self, response: GenerationResponse, candidate: Candidate) -> ChatMessageContent:
"""Create a chat message content object.
Args:
response: The response from the service.
candidate: The candidate from the response.
Returns:
A chat message content object.
"""
# Best effort conversion of finish reason. The raw value will be available in metadata.
finish_reason: FinishReason | None = finish_reason_from_vertex_ai_to_semantic_kernel(candidate.finish_reason)
response_metadata = self._get_metadata_from_response(response)
response_metadata.update(self._get_metadata_from_candidate(candidate))
items: list[CMC_ITEM_TYPES] = []
for idx, part in enumerate(candidate.content.parts):
part_dict = part.to_dict()
if "text" in part_dict:
items.append(TextContent(text=part.text, inner_content=response, metadata=response_metadata))
elif "function_call" in part_dict:
fc_metadata: dict[str, Any] = {}
thought_sig = part_dict.get("thought_signature")
if thought_sig:
fc_metadata["thought_signature"] = thought_sig
items.append(
FunctionCallContent(
id=f"{part.function_call.name}_{idx!s}",
name=format_gemini_function_name_to_kernel_function_fully_qualified_name(
part.function_call.name
),
arguments={k: v for k, v in part.function_call.args.items()},
metadata=fc_metadata if fc_metadata else None,
)
)
return ChatMessageContent(
ai_model_id=self.ai_model_id,
role=AuthorRole.ASSISTANT,
items=items,
inner_content=response,
finish_reason=finish_reason,
metadata=response_metadata,
)
# endregion
# region Streaming
def _create_streaming_chat_message_content(
self,
chunk: GenerationResponse,
candidate: Candidate,
function_invoke_attempt: int,
) -> StreamingChatMessageContent:
"""Create a streaming chat message content object.
Args:
chunk: The response from the service.
candidate: The candidate from the response.
function_invoke_attempt: The function invoke attempt.
Returns:
A streaming chat message content object.
"""
# Best effort conversion of finish reason. The raw value will be available in metadata.
finish_reason: FinishReason | None = finish_reason_from_vertex_ai_to_semantic_kernel(candidate.finish_reason)
response_metadata = self._get_metadata_from_response(chunk)
response_metadata.update(self._get_metadata_from_candidate(candidate))
items: list[STREAMING_ITEM_TYPES] = []
for idx, part in enumerate(candidate.content.parts):
part_dict = part.to_dict()
if "text" in part_dict:
items.append(
StreamingTextContent(
choice_index=candidate.index,
text=part.text,
inner_content=chunk,
metadata=response_metadata,
)
)
elif "function_call" in part_dict:
fc_metadata_s: dict[str, Any] = {}
thought_sig_s = part_dict.get("thought_signature")
if thought_sig_s:
fc_metadata_s["thought_signature"] = thought_sig_s
items.append(
FunctionCallContent(
id=f"{part.function_call.name}_{idx!s}",
name=format_gemini_function_name_to_kernel_function_fully_qualified_name(
part.function_call.name
),
arguments={k: v for k, v in part.function_call.args.items()},
metadata=fc_metadata_s if fc_metadata_s else None,
)
)
return StreamingChatMessageContent(
ai_model_id=self.ai_model_id,
role=AuthorRole.ASSISTANT,
choice_index=candidate.index,
items=items,
inner_content=chunk,
finish_reason=finish_reason,
metadata=response_metadata,
function_invoke_attempt=function_invoke_attempt,
)
# endregion
def _get_metadata_from_response(self, response: GenerationResponse) -> dict[str, Any]:
"""Get metadata from the response.
Args:
response: The response from the service.
Returns:
A dictionary containing metadata.
"""
return {
"prompt_feedback": response.prompt_feedback,
"usage": CompletionUsage(
prompt_tokens=response.usage_metadata.prompt_token_count,
completion_tokens=response.usage_metadata.candidates_token_count,
),
}
def _get_metadata_from_candidate(self, candidate: Candidate) -> dict[str, Any]:
"""Get metadata from the candidate.
Args:
candidate: The candidate from the response.
Returns:
A dictionary containing metadata.
"""
return {
"index": candidate.index,
"finish_reason": candidate.finish_reason,
"safety_ratings": candidate.safety_ratings,
}
@@ -0,0 +1,211 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from collections.abc import AsyncGenerator, AsyncIterable
from typing import Any
import vertexai
from pydantic import ValidationError
from typing_extensions import deprecated
from vertexai.generative_models import Candidate, GenerationResponse, GenerativeModel
from semantic_kernel.connectors.ai.completion_usage import CompletionUsage
from semantic_kernel.connectors.ai.google.vertex_ai.services.vertex_ai_base import VertexAIBase
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_prompt_execution_settings import (
VertexAITextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_settings import VertexAISettings
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
from semantic_kernel.utils.telemetry.model_diagnostics.decorators import (
trace_streaming_text_completion,
trace_text_completion,
)
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
@deprecated(
"VertexAITextCompletion is deprecated and will be removed after 01/01/2026. "
"Use `semantic_kernel.connectors.ai.google.GoogleAITextCompletion` connectors instead."
)
class VertexAITextCompletion(VertexAIBase, TextCompletionClientBase):
"""Vertex AI Text Completion Client."""
def __init__(
self,
project_id: str | None = None,
region: str | None = None,
gemini_model_id: str | None = None,
service_id: str | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize the Google Vertex AI Text Completion Service.
If no arguments are provided, the service will attempt to load the settings from the environment.
The following environment variables are used:
- VERTEX_AI_GEMINI_MODEL_ID
- VERTEX_AI_PROJECT_ID
Args:
project_id (str): The Google Cloud project ID.
region (str): The Google Cloud region.
gemini_model_id (str): The Gemini model ID.
service_id (str): The Vertex AI service ID.
env_file_path (str): The path to the environment file.
env_file_encoding (str): The encoding of the environment file.
"""
try:
vertex_ai_settings = VertexAISettings(
project_id=project_id,
region=region,
gemini_model_id=gemini_model_id,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as e:
raise ServiceInitializationError(f"Failed to validate Vertex AI settings: {e}") from e
if not vertex_ai_settings.gemini_model_id:
raise ServiceInitializationError("The Vertex AI Gemini model ID is required.")
super().__init__(
ai_model_id=vertex_ai_settings.gemini_model_id,
service_id=service_id or vertex_ai_settings.gemini_model_id,
service_settings=vertex_ai_settings,
)
# region Overriding base class methods
# Override from AIServiceClientBase
@override
def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]:
return VertexAITextPromptExecutionSettings
@override
@trace_text_completion(VertexAIBase.MODEL_PROVIDER_NAME)
async def _inner_get_text_contents(
self,
prompt: str,
settings: "PromptExecutionSettings",
) -> list[TextContent]:
if not isinstance(settings, VertexAITextPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, VertexAITextPromptExecutionSettings) # nosec
vertexai.init(project=self.service_settings.project_id, location=self.service_settings.region)
assert self.service_settings.gemini_model_id is not None # nosec
model = GenerativeModel(self.service_settings.gemini_model_id)
response: GenerationResponse = await model.generate_content_async(
contents=prompt,
generation_config=settings.prepare_settings_dict(),
)
return [self._create_text_content(response, candidate) for candidate in response.candidates]
@override
@trace_streaming_text_completion(VertexAIBase.MODEL_PROVIDER_NAME)
async def _inner_get_streaming_text_contents(
self,
prompt: str,
settings: "PromptExecutionSettings",
) -> AsyncGenerator[list[StreamingTextContent], Any]:
if not isinstance(settings, VertexAITextPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, VertexAITextPromptExecutionSettings) # nosec
vertexai.init(project=self.service_settings.project_id, location=self.service_settings.region)
assert self.service_settings.gemini_model_id is not None # nosec
model = GenerativeModel(self.service_settings.gemini_model_id)
response: AsyncIterable[GenerationResponse] = await model.generate_content_async(
contents=prompt,
generation_config=settings.prepare_settings_dict(),
stream=True,
)
async for chunk in response:
yield [self._create_streaming_text_content(chunk, candidate) for candidate in chunk.candidates]
# endregion
def _create_text_content(self, response: GenerationResponse, candidate: Candidate) -> TextContent:
"""Create a text content object.
Args:
response: The response from the service.
candidate: The candidate from the response.
Returns:
A text content object.
"""
response_metadata = self._get_metadata_from_response(response)
response_metadata.update(self._get_metadata_from_candidate(candidate))
return TextContent(
ai_model_id=self.ai_model_id,
text=candidate.content.parts[0].text,
inner_content=response,
metadata=response_metadata,
)
def _create_streaming_text_content(self, chunk: GenerationResponse, candidate: Candidate) -> StreamingTextContent:
"""Create a streaming text content object.
Args:
chunk: The response from the service.
candidate: The candidate from the response.
Returns:
A streaming text content object.
"""
response_metadata = self._get_metadata_from_response(chunk)
response_metadata.update(self._get_metadata_from_candidate(candidate))
return StreamingTextContent(
ai_model_id=self.ai_model_id,
choice_index=candidate.index,
text=candidate.content.parts[0].text,
inner_content=chunk,
metadata=response_metadata,
)
def _get_metadata_from_response(self, response: GenerationResponse) -> dict[str, Any]:
"""Get metadata from the response.
Args:
response: The response from the service.
Returns:
A dictionary containing metadata.
"""
return {
"prompt_feedback": response.prompt_feedback,
"usage": CompletionUsage(
prompt_tokens=response.usage_metadata.prompt_token_count,
completion_tokens=response.usage_metadata.candidates_token_count,
),
}
def _get_metadata_from_candidate(self, candidate: Candidate) -> dict[str, Any]:
"""Get metadata from the candidate.
Args:
candidate: The candidate from the response.
Returns:
A dictionary containing metadata.
"""
return {
"index": candidate.index,
"finish_reason": candidate.finish_reason,
"safety_ratings": candidate.safety_ratings,
}
@@ -0,0 +1,116 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from typing import Any
import vertexai
from numpy import array, ndarray
from pydantic import ValidationError
from vertexai.language_models import TextEmbedding, TextEmbeddingModel
from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase
from semantic_kernel.connectors.ai.google.vertex_ai.services.vertex_ai_base import VertexAIBase
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_prompt_execution_settings import (
VertexAIEmbeddingPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_settings import VertexAISettings
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
from typing_extensions import deprecated
@deprecated(
"VertexAITextEmbedding is deprecated and will be removed after 01/01/2026. "
"Use `semantic_kernel.connectors.ai.google.GoogleAITextEmbedding` connectors instead."
)
class VertexAITextEmbedding(VertexAIBase, EmbeddingGeneratorBase):
"""Vertex AI Text Embedding Service."""
def __init__(
self,
project_id: str | None = None,
region: str | None = None,
embedding_model_id: str | None = None,
service_id: str | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize the Google Vertex AI 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:
- VERTEX_AI_EMBEDDING_MODEL_ID
- VERTEX_AI_PROJECT_ID
Args:
project_id (str): The Google Cloud project ID.
region (str): The Google Cloud region.
embedding_model_id (str): The Gemini model ID.
service_id (str): The Vertex AI service ID.
env_file_path (str): The path to the environment file.
env_file_encoding (str): The encoding of the environment file.
"""
try:
vertex_ai_settings = VertexAISettings(
project_id=project_id,
region=region,
embedding_model_id=embedding_model_id,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as e:
raise ServiceInitializationError(f"Failed to validate Vertex AI settings: {e}") from e
if not vertex_ai_settings.embedding_model_id:
raise ServiceInitializationError("The Vertex AI embedding model ID is required.")
super().__init__(
ai_model_id=vertex_ai_settings.embedding_model_id,
service_id=service_id or vertex_ai_settings.embedding_model_id,
service_settings=vertex_ai_settings,
)
@override
async def generate_embeddings(
self,
texts: list[str],
settings: "PromptExecutionSettings | None" = None,
**kwargs: Any,
) -> ndarray:
raw_embeddings = await self.generate_raw_embeddings(texts, settings, **kwargs)
return array(raw_embeddings)
@override
async def generate_raw_embeddings(
self,
texts: list[str],
settings: "PromptExecutionSettings | None" = None,
**kwargs: Any,
) -> list[list[float]]:
if not settings:
settings = VertexAIEmbeddingPromptExecutionSettings()
else:
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, VertexAIEmbeddingPromptExecutionSettings) # nosec
vertexai.init(project=self.service_settings.project_id, location=self.service_settings.region)
assert self.service_settings.embedding_model_id is not None # nosec
model = TextEmbeddingModel.from_pretrained(self.service_settings.embedding_model_id)
response: list[TextEmbedding] = await model.get_embeddings_async(
texts, # type: ignore[arg-type]
**settings.prepare_settings_dict(),
)
return [text_embedding.values for text_embedding in response]
@override
def get_prompt_execution_settings_class(
self,
) -> type["PromptExecutionSettings"]:
"""Get the request settings class."""
return VertexAIEmbeddingPromptExecutionSettings
@@ -0,0 +1,89 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from typing import Annotated, Any, Literal
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
from pydantic import Field
from typing_extensions import deprecated
from vertexai.generative_models import Tool, ToolConfig
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
@deprecated(
"VertexAIPromptExecutionSettings is deprecated and will be removed after 01/01/2026. "
"Use google_ai connectors instead."
)
class VertexAIPromptExecutionSettings(PromptExecutionSettings):
"""Vertex AI Prompt Execution Settings."""
stop_sequences: Annotated[list[str] | None, Field(max_length=5)] = None
response_mime_type: Literal["text/plain", "application/json"] | None = None
response_schema: Any | None = None
candidate_count: Annotated[int | None, Field(ge=1)] = None
max_output_tokens: Annotated[int | None, Field(ge=1)] = None
temperature: Annotated[float | None, Field(ge=0.0, le=2.0)] = None
top_p: float | None = None
top_k: int | None = None
@deprecated(
"VertexAITextPromptExecutionSettings is deprecated and will be removed after 01/01/2026. "
"Use google_ai connectors instead."
)
class VertexAITextPromptExecutionSettings(VertexAIPromptExecutionSettings):
"""Vertex AI Text Prompt Execution Settings."""
pass
@deprecated(
"VertexAIChatPromptExecutionSettings is deprecated and will be removed after 01/01/2026. "
"Use google_ai connectors instead."
)
class VertexAIChatPromptExecutionSettings(VertexAIPromptExecutionSettings):
"""Vertex AI Chat Prompt Execution Settings."""
tools: Annotated[
list[Tool] | None,
Field(
description="Do not set this manually. It is set by the service based "
"on the function choice configuration.",
),
] = None
tool_config: Annotated[
ToolConfig | None,
Field(
description="Do not set this manually. It is set by the service based "
"on the function choice configuration.",
),
] = None
@override
def prepare_settings_dict(self, **kwargs) -> dict[str, Any]:
"""Prepare the settings as a dictionary for sending to the AI service.
This method removes the tools and tool_config keys from the settings dictionary, as
the Vertex AI service mandates these two settings to be sent as separate parameters.
"""
settings_dict = super().prepare_settings_dict(**kwargs)
settings_dict.pop("tools", None)
settings_dict.pop("tool_config", None)
return settings_dict
@deprecated(
"VertexAIEmbeddingPromptExecutionSettings is deprecated and will be removed after 01/01/2026. "
"Use google_ai connectors instead."
)
class VertexAIEmbeddingPromptExecutionSettings(PromptExecutionSettings):
"""Google AI Embedding Prompt Execution Settings."""
auto_truncate: bool | None = None
output_dimensionality: int | None = None
@@ -0,0 +1,40 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import ClassVar
from typing_extensions import deprecated
from semantic_kernel.kernel_pydantic import KernelBaseSettings
@deprecated("VertexAISettings is deprecated and will be removed after 01/01/2026. Use google_ai connectors instead.")
class VertexAISettings(KernelBaseSettings):
"""Vertex AI settings.
The settings are first loaded from environment variables with
the prefix 'VERTEX_AI_'.
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 'VERTEX_AI_' are:
- gemini_model_id: str - The Gemini model ID for the Vertex AI service, i.e. gemini-1.5-pro
This value can be found in the Vertex AI service deployment.
(Env var VERTEX_AI_GEMINI_MODEL_ID)
- embedding_model_id: str - The embedding model ID for the Vertex AI service, i.e. text-embedding-004
This value can be found in the Vertex AI service deployment.
(Env var VERTEX_AI_EMBEDDING_MODEL_ID)
- project_id: str - The Google Cloud project ID.
(Env var VERTEX_AI_PROJECT_ID)
- region: str - The Google Cloud region.
(Env var VERTEX_AI_REGION)
"""
env_prefix: ClassVar[str] = "VERTEX_AI_"
gemini_model_id: str | None = None
embedding_model_id: str | None = None
project_id: str
region: str | None = None
@@ -0,0 +1,17 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.hugging_face.hf_prompt_execution_settings import (
HuggingFacePromptExecutionSettings,
)
from semantic_kernel.connectors.ai.hugging_face.services.hf_text_completion import (
HuggingFaceTextCompletion,
)
from semantic_kernel.connectors.ai.hugging_face.services.hf_text_embedding import (
HuggingFaceTextEmbedding,
)
__all__ = [
"HuggingFacePromptExecutionSettings",
"HuggingFaceTextCompletion",
"HuggingFaceTextEmbedding",
]
@@ -0,0 +1,53 @@
# Copyright (c) Microsoft. All rights reserved.
import importlib
from typing import TYPE_CHECKING, Any
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
if TYPE_CHECKING:
from transformers import GenerationConfig
imported = importlib.import_module("transformers")
ready = imported is not None and hasattr(imported, "GenerationConfig")
class HuggingFacePromptExecutionSettings(PromptExecutionSettings):
"""Hugging Face prompt execution settings."""
do_sample: bool = True
max_new_tokens: int = 256
num_return_sequences: int = 1
stop_sequences: Any = None
pad_token_id: int = 50256
eos_token_id: int = 50256
temperature: float = 1.0
top_p: float = 1.0
def get_generation_config(self) -> "GenerationConfig":
"""Get the generation config."""
from transformers import GenerationConfig
if not ready:
raise ImportError("transformers is not installed.")
return GenerationConfig(
**self.model_dump(
include={"max_new_tokens", "pad_token_id", "eos_token_id", "temperature", "top_p"},
exclude_unset=False,
exclude_none=True,
by_alias=True,
)
)
def prepare_settings_dict(self, **kwargs) -> dict[str, Any]:
"""Prepare the settings dictionary."""
gen_config = self.get_generation_config()
settings = {
"generation_config": gen_config,
"num_return_sequences": self.num_return_sequences,
"do_sample": self.do_sample,
}
settings.update(kwargs)
return settings
@@ -0,0 +1 @@
# Copyright (c) Microsoft. All rights reserved.
@@ -0,0 +1,157 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
import sys
from collections.abc import AsyncGenerator
from threading import Thread
from typing import Any, ClassVar, Literal
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
import torch
from transformers import AutoTokenizer, TextIteratorStreamer, pipeline
from semantic_kernel.connectors.ai.hugging_face.hf_prompt_execution_settings import HuggingFacePromptExecutionSettings
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase
from semantic_kernel.contents.streaming_text_content import StreamingTextContent
from semantic_kernel.contents.text_content import TextContent
from semantic_kernel.exceptions import ServiceInvalidExecutionSettingsError, ServiceResponseException
from semantic_kernel.utils.telemetry.model_diagnostics.decorators import (
trace_streaming_text_completion,
trace_text_completion,
)
logger: logging.Logger = logging.getLogger(__name__)
class HuggingFaceTextCompletion(TextCompletionClientBase):
"""Hugging Face text completion service."""
MODEL_PROVIDER_NAME: ClassVar[str] = "huggingface"
task: Literal["summarization", "text-generation", "text2text-generation"]
device: str
generator: Any
def __init__(
self,
ai_model_id: str,
task: str | None = "text2text-generation",
device: int = -1,
service_id: str | None = None,
model_kwargs: dict[str, Any] | None = None,
pipeline_kwargs: dict[str, Any] | None = None,
) -> None:
"""Initializes a new instance of the HuggingFaceTextCompletion class.
Args:
ai_model_id (str): Hugging Face model card string, see
https://huggingface.co/models
device (int): Device to run the model on, defaults to CPU, 0+ for GPU,
-- None if using device_map instead. (If both device and device_map
are specified, device overrides device_map. If unintended,
it can lead to unexpected behavior.) (optional)
service_id (str): Service ID for the AI service. (optional)
task (str): Model completion task type, options are:
- summarization: takes a long text and returns a shorter summary.
- text-generation: takes incomplete text and returns a set of completion candidates.
- text2text-generation (default): takes an input prompt and returns a completion.
text2text-generation is the default as it behaves more like GPT-3+. (optional)
model_kwargs (dict[str, Any]): Additional dictionary of keyword arguments
passed along to the model's `from_pretrained(..., **model_kwargs)` function. (optional)
pipeline_kwargs (dict[str, Any]): Additional keyword arguments passed along
to the specific pipeline init (see the documentation for the corresponding pipeline class
for possible values). (optional)
Note that this model will be downloaded from the Hugging Face model hub.
"""
generator = pipeline(
task=task, # type: ignore[arg-type]
model=ai_model_id,
device=device,
model_kwargs=model_kwargs,
**pipeline_kwargs or {},
)
resolved_device = f"cuda:{device}" if device >= 0 and torch.cuda.is_available() else "cpu"
super().__init__(
service_id=service_id or ai_model_id,
ai_model_id=ai_model_id,
task=task,
device=resolved_device,
generator=generator,
)
# region Overriding base class methods
# Override from AIServiceClientBase
@override
def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]:
return HuggingFacePromptExecutionSettings
@override
@trace_text_completion(MODEL_PROVIDER_NAME)
async def _inner_get_text_contents(
self,
prompt: str,
settings: "PromptExecutionSettings",
) -> list[TextContent]:
if not isinstance(settings, HuggingFacePromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, HuggingFacePromptExecutionSettings) # nosec
try:
results = self.generator(prompt, **settings.prepare_settings_dict())
except Exception as e:
raise ServiceResponseException("Hugging Face completion failed") from e
if isinstance(results, list):
return [self._create_text_content(results, result) for result in results]
return [self._create_text_content(results, results)]
@override
@trace_streaming_text_completion(MODEL_PROVIDER_NAME)
async def _inner_get_streaming_text_contents(
self,
prompt: str,
settings: "PromptExecutionSettings",
) -> AsyncGenerator[list[StreamingTextContent], Any]:
if not isinstance(settings, HuggingFacePromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, HuggingFacePromptExecutionSettings) # nosec
if settings.num_return_sequences > 1:
raise ServiceInvalidExecutionSettingsError(
"HuggingFace TextIteratorStreamer does not stream multiple responses in a parsable format."
" If you need multiple responses, please use the complete method.",
)
try:
streamer = TextIteratorStreamer(AutoTokenizer.from_pretrained(self.ai_model_id))
# See https://github.com/huggingface/transformers/blob/main/src/transformers/generation/streamers.py#L159
thread = Thread(
target=self.generator, args={prompt}, kwargs=settings.prepare_settings_dict(streamer=streamer)
)
thread.start()
for new_text in streamer:
yield [
StreamingTextContent(
choice_index=0, inner_content=new_text, text=new_text, ai_model_id=self.ai_model_id
)
]
thread.join()
except Exception as e:
raise ServiceResponseException("Hugging Face completion failed") from e
# endregion
def _create_text_content(self, response: Any, candidate: dict[str, str]) -> TextContent:
return TextContent(
inner_content=response,
ai_model_id=self.ai_model_id,
text=candidate["summary_text" if self.task == "summarization" else "generated_text"],
)
@@ -0,0 +1,88 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
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
import torch
from numpy import ndarray
from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase
from semantic_kernel.exceptions import ServiceResponseException
from semantic_kernel.utils.feature_stage_decorator import experimental
if TYPE_CHECKING:
from torch import Tensor
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
logger: logging.Logger = logging.getLogger(__name__)
@experimental
class HuggingFaceTextEmbedding(EmbeddingGeneratorBase):
"""Hugging Face text embedding service."""
device: str
generator: Any
def __init__(
self,
ai_model_id: str,
device: int = -1,
service_id: str | None = None,
) -> None:
"""Initializes a new instance of the HuggingFaceTextEmbedding class.
Args:
ai_model_id (str): Hugging Face model card string, see
https://huggingface.co/sentence-transformers
device (int): Device to run the model on, -1 for CPU, 0+ for GPU. (optional)
service_id (str): Service ID for the model. (optional)
Note that this model will be downloaded from the Hugging Face model hub.
"""
from sentence_transformers import SentenceTransformer
resolved_device = f"cuda:{device}" if device >= 0 and torch.cuda.is_available() else "cpu"
super().__init__(
ai_model_id=ai_model_id,
service_id=service_id or ai_model_id,
device=resolved_device,
generator=SentenceTransformer( # type: ignore
model_name_or_path=ai_model_id,
device=resolved_device,
),
)
@override
async def generate_embeddings(
self,
texts: list[str],
settings: "PromptExecutionSettings | None" = None,
**kwargs: Any,
) -> ndarray:
try:
logger.info(f"Generating embeddings for {len(texts)} texts.")
return self.generator.encode(sentences=texts, convert_to_numpy=True, **kwargs)
except Exception as e:
raise ServiceResponseException("Hugging Face embeddings failed", e) from e
@override
async def generate_raw_embeddings(
self,
texts: list[str],
settings: "PromptExecutionSettings | None" = None,
**kwargs: Any,
) -> "list[Tensor] | ndarray | Tensor":
try:
logger.info(f"Generating raw embeddings for {len(texts)} texts.")
return self.generator.encode(sentences=texts, **kwargs)
except Exception as e:
raise ServiceResponseException("Hugging Face embeddings failed", e) from e
@@ -0,0 +1,15 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.mistral_ai.prompt_execution_settings.mistral_ai_prompt_execution_settings import (
MistralAIChatPromptExecutionSettings,
MistralAIPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.mistral_ai.services.mistral_ai_chat_completion import MistralAIChatCompletion
from semantic_kernel.connectors.ai.mistral_ai.services.mistral_ai_text_embedding import MistralAITextEmbedding
__all__ = [
"MistralAIChatCompletion",
"MistralAIChatPromptExecutionSettings",
"MistralAIPromptExecutionSettings",
"MistralAITextEmbedding",
]
@@ -0,0 +1,58 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
from typing import Annotated, Any, Literal
from mistralai import utils
from pydantic import Field
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
logger = logging.getLogger(__name__)
class MistralAIPromptExecutionSettings(PromptExecutionSettings):
"""Common request settings for MistralAI services."""
ai_model_id: Annotated[str | None, Field(serialization_alias="model")] = None
class MistralAIChatPromptExecutionSettings(MistralAIPromptExecutionSettings):
"""Specific settings for the Chat Completion endpoint."""
response_format: dict[Literal["type"], Literal["text", "json_object"]] | None = None
messages: list[dict[str, Any]] | None = None
safe_mode: Annotated[
bool,
Field(
exclude=True,
deprecated="The 'safe_mode' setting is no longer supported and is being ignored, "
"it will be removed in the Future.",
),
] = False
safe_prompt: bool = False
max_tokens: Annotated[int | None, Field(gt=0)] = None
seed: int | None = None
temperature: Annotated[float | None, Field(ge=0.0, le=2.0)] = None
top_p: Annotated[float | None, Field(ge=0.0, le=1.0)] = None
random_seed: int | None = None
presence_penalty: Annotated[float | None, Field(gt=0)] = None
frequency_penalty: Annotated[float | None, Field(gt=0)] = None
n: Annotated[int | None, Field(gt=1)] = None
retries: utils.RetryConfig | None = None
server_url: str | None = None
timeout_ms: int | None = None
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
@@ -0,0 +1,16 @@
# Copyright (c) Microsoft. All rights reserved.
from abc import ABC
from typing import ClassVar
from mistralai import Mistral
from semantic_kernel.kernel_pydantic import KernelBaseModel
class MistralAIBase(KernelBaseModel, ABC):
"""Mistral AI service base."""
MODEL_PROVIDER_NAME: ClassVar[str] = "mistralai"
async_client: Mistral
@@ -0,0 +1,328 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
import sys
from collections.abc import AsyncGenerator, Callable
from typing import TYPE_CHECKING, Any, ClassVar
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
from mistralai import Mistral
from mistralai.models import (
AssistantMessage,
ChatCompletionChoice,
ChatCompletionResponse,
CompletionChunk,
CompletionResponseStreamChoice,
DeltaMessage,
ToolCall,
)
from pydantic import ValidationError
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 kernel_function_metadata_to_function_call_format
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType
from semantic_kernel.connectors.ai.mistral_ai.prompt_execution_settings.mistral_ai_prompt_execution_settings import (
MistralAIChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.mistral_ai.services.mistral_ai_base import MistralAIBase
from semantic_kernel.connectors.ai.mistral_ai.settings.mistral_ai_settings import MistralAISettings
from semantic_kernel.contents import (
ChatMessageContent,
FunctionCallContent,
StreamingChatMessageContent,
StreamingTextContent,
TextContent,
)
from semantic_kernel.contents.chat_history import ChatHistory
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 ServiceInitializationError, ServiceResponseException
from semantic_kernel.utils.feature_stage_decorator import experimental
from semantic_kernel.utils.telemetry.model_diagnostics.decorators import (
trace_chat_completion,
trace_streaming_chat_completion,
)
if TYPE_CHECKING:
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
logger: logging.Logger = logging.getLogger(__name__)
@experimental
class MistralAIChatCompletion(MistralAIBase, ChatCompletionClientBase):
"""Mistral Chat completion class."""
SUPPORTS_FUNCTION_CALLING: ClassVar[bool] = True
def __init__(
self,
ai_model_id: str | None = None,
service_id: str | None = None,
api_key: str | None = None,
async_client: Mistral | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize an MistralAIChatCompletion service.
Args:
ai_model_id : MistralAI model name, see
https://docs.mistral.ai/getting-started/models/
service_id : Service ID tied to the execution settings.
api_key : The optional API key to use. If provided will override,
the env vars or .env file value.
async_client : An existing client to use.
env_file_path : Use the environment settings file as a fallback
to environment variables.
env_file_encoding : The encoding of the environment settings file.
"""
try:
mistralai_settings = MistralAISettings(
api_key=api_key,
chat_model_id=ai_model_id,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as ex:
raise ServiceInitializationError("Failed to create MistralAI settings.", ex) from ex
if not mistralai_settings.chat_model_id:
raise ServiceInitializationError("The MistralAI chat model ID is required.")
if not async_client:
async_client = Mistral(
api_key=mistralai_settings.api_key.get_secret_value(),
)
super().__init__(
async_client=async_client,
service_id=service_id or mistralai_settings.chat_model_id,
ai_model_id=ai_model_id or mistralai_settings.chat_model_id,
)
# region Overriding base class methods
# Override from AIServiceClientBase
@override
def get_prompt_execution_settings_class(self) -> "type[MistralAIChatPromptExecutionSettings]":
"""Create a request settings object."""
return MistralAIChatPromptExecutionSettings
# Override from AIServiceClientBase
@override
def service_url(self) -> str | None:
if hasattr(self.async_client, "_endpoint"):
# Best effort to get the endpoint
return self.async_client._endpoint
return None
@override
@trace_chat_completion(MistralAIBase.MODEL_PROVIDER_NAME)
async def _inner_get_chat_message_contents(
self,
chat_history: "ChatHistory",
settings: "PromptExecutionSettings",
) -> list["ChatMessageContent"]:
if not isinstance(settings, MistralAIChatPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, MistralAIChatPromptExecutionSettings) # nosec
settings.ai_model_id = settings.ai_model_id or self.ai_model_id
settings.messages = self._prepare_chat_history_for_request(chat_history)
try:
response = await self.async_client.chat.complete_async(**settings.prepare_settings_dict())
except Exception as ex:
raise ServiceResponseException(
f"{type(self)} service failed to complete the prompt",
ex,
) from ex
if isinstance(response, ChatCompletionResponse):
response_metadata = self._get_metadata_from_response(response)
# If there are no choices, return an empty list
if isinstance(response.choices, list) and len(response.choices) > 0:
return [
self._create_chat_message_content(response, choice, response_metadata)
for choice in response.choices
]
return []
@override
@trace_streaming_chat_completion(MistralAIBase.MODEL_PROVIDER_NAME)
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, MistralAIChatPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, MistralAIChatPromptExecutionSettings) # nosec
settings.ai_model_id = settings.ai_model_id or self.ai_model_id
settings.messages = self._prepare_chat_history_for_request(chat_history)
try:
response = await self.async_client.chat.stream_async(**settings.prepare_settings_dict())
except Exception as ex:
raise ServiceResponseException(
f"{type(self)} service failed to complete the prompt",
ex,
) from ex
# If there is no response end the generator
if isinstance(response, AsyncGenerator):
async for chunk in response:
if len(chunk.data.choices) == 0:
continue
chunk_metadata = self._get_metadata_from_response(chunk.data)
yield [
self._create_streaming_chat_message_content(
chunk.data, choice, chunk_metadata, function_invoke_attempt
)
for choice in chunk.data.choices
]
# endregion
# region content conversion to SK
def _create_chat_message_content(
self, response: ChatCompletionResponse, choice: ChatCompletionChoice, response_metadata: dict[str, Any]
) -> "ChatMessageContent":
"""Create a chat message content object from a choice."""
metadata = self._get_metadata_from_chat_choice(choice)
metadata.update(response_metadata)
items: list[Any] = self._get_tool_calls_from_chat_choice(choice)
if choice.message.content:
items.append(TextContent(text=choice.message.content))
return ChatMessageContent(
inner_content=response,
ai_model_id=self.ai_model_id,
metadata=metadata,
role=AuthorRole(choice.message.role),
items=items,
finish_reason=FinishReason(choice.finish_reason) if choice.finish_reason else None,
)
def _create_streaming_chat_message_content(
self,
chunk: CompletionChunk,
choice: CompletionResponseStreamChoice,
chunk_metadata: dict[str, Any],
function_invoke_attempt: int,
) -> StreamingChatMessageContent:
"""Create a streaming chat message content object from a choice."""
metadata = self._get_metadata_from_chat_choice(choice)
metadata.update(chunk_metadata)
items: list[Any] = self._get_tool_calls_from_chat_choice(choice)
if choice.delta.content is not None:
items.append(StreamingTextContent(choice_index=choice.index, text=choice.delta.content))
return StreamingChatMessageContent(
choice_index=choice.index,
inner_content=chunk,
ai_model_id=self.ai_model_id,
metadata=metadata,
role=AuthorRole(choice.delta.role) if choice.delta.role else AuthorRole.ASSISTANT,
finish_reason=FinishReason(choice.finish_reason) if choice.finish_reason else None,
items=items,
function_invoke_attempt=function_invoke_attempt,
)
def _get_metadata_from_response(self, response: ChatCompletionResponse | CompletionChunk) -> dict[str, Any]:
"""Get metadata from a chat response."""
metadata: dict[str, Any] = {
"id": response.id,
"created": response.created,
}
# Check if usage exists and has a value, then add it to the metadata
if hasattr(response, "usage") and response.usage is not None:
metadata["usage"] = (
CompletionUsage(
prompt_tokens=response.usage.prompt_tokens,
completion_tokens=response.usage.completion_tokens,
),
)
return metadata
def _get_metadata_from_chat_choice(
self, choice: ChatCompletionChoice | CompletionResponseStreamChoice
) -> dict[str, Any]:
"""Get metadata from a chat choice."""
return {
"logprobs": getattr(choice, "logprobs", None),
}
def _get_tool_calls_from_chat_choice(
self, choice: ChatCompletionChoice | CompletionResponseStreamChoice
) -> list[FunctionCallContent]:
"""Get tool calls from a chat choice."""
content: AssistantMessage | DeltaMessage
content = choice.message if isinstance(choice, ChatCompletionChoice) else choice.delta
if content.tool_calls is None:
return []
return [
FunctionCallContent(
id=tool.id,
index=getattr(tool, "index", None),
name=tool.function.name,
arguments=tool.function.arguments,
)
for tool in content.tool_calls
if isinstance(tool, ToolCall)
]
# endregion
def update_settings_from_function_call_configuration_mistral(
self,
function_choice_configuration: "FunctionCallChoiceConfiguration",
settings: "PromptExecutionSettings",
type: "FunctionChoiceType",
) -> None:
"""Update the settings from a FunctionChoiceConfiguration."""
if (
function_choice_configuration.available_functions
and hasattr(settings, "tool_choice")
and hasattr(settings, "tools")
):
settings.tool_choice = type
settings.tools = [
kernel_function_metadata_to_function_call_format(f)
for f in function_choice_configuration.available_functions
]
# Function Choice behavior required maps to MistralAI any
if (
settings.function_choice_behavior
and settings.function_choice_behavior.type_ == FunctionChoiceType.REQUIRED
):
settings.tool_choice = "any"
@override
def _update_function_choice_settings_callback(
self,
) -> Callable[["FunctionCallChoiceConfiguration", "PromptExecutionSettings", FunctionChoiceType], None]:
return self.update_settings_from_function_call_configuration_mistral
@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
@@ -0,0 +1,108 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
if sys.version_info >= (3, 12):
from typing import Any, override # pragma: no cover
else:
from typing_extensions import Any, override # pragma: no cover
import logging
from mistralai import Mistral
from mistralai.models import EmbeddingResponse
from numpy import array, ndarray
from pydantic import ValidationError
from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase
from semantic_kernel.connectors.ai.mistral_ai.services.mistral_ai_base import MistralAIBase
from semantic_kernel.connectors.ai.mistral_ai.settings.mistral_ai_settings import MistralAISettings
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError, ServiceResponseException
from semantic_kernel.utils.feature_stage_decorator import experimental
logger: logging.Logger = logging.getLogger(__name__)
@experimental
class MistralAITextEmbedding(MistralAIBase, EmbeddingGeneratorBase):
"""Mistral AI Inference Text Embedding Service."""
def __init__(
self,
ai_model_id: str | None = None,
api_key: str | None = None,
service_id: str | None = None,
async_client: Mistral | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize the Mistral AI 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:
- MISTRALAI_API_KEY
- MISTRALAI_EMBEDDING_MODEL_ID
Args:
ai_model_id: : A string that is used to identify the model such as the model name.
api_key : The API key for the Mistral AI service deployment.
service_id : Service ID for the embedding completion service.
async_client : The Mistral AI client to use.
env_file_path : The path to the environment file.
env_file_encoding : The encoding of the environment file.
Raises:
ServiceInitializationError: If an error occurs during initialization.
"""
try:
mistralai_settings = MistralAISettings(
api_key=api_key,
embedding_model_id=ai_model_id,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as e:
raise ServiceInitializationError(f"Failed to validate Mistral AI settings: {e}") from e
if not mistralai_settings.embedding_model_id:
raise ServiceInitializationError("The MistralAI embedding model ID is required.")
if not async_client:
async_client = Mistral(
api_key=mistralai_settings.api_key.get_secret_value(),
)
super().__init__(
service_id=service_id or mistralai_settings.embedding_model_id,
ai_model_id=ai_model_id or mistralai_settings.embedding_model_id,
async_client=async_client,
)
@override
async def generate_embeddings(
self,
texts: list[str],
settings: "PromptExecutionSettings | None" = None,
**kwargs: Any,
) -> ndarray:
embedding_response = await self.generate_raw_embeddings(texts, settings, **kwargs)
return array(embedding_response)
@override
async def generate_raw_embeddings(
self,
texts: list[str],
settings: "PromptExecutionSettings | None" = None,
**kwargs: Any,
) -> Any:
"""Generate embeddings from the Mistral AI service."""
try:
embedding_response = await self.async_client.embeddings.create_async(model=self.ai_model_id, inputs=texts)
except Exception as ex:
raise ServiceResponseException(
f"{type(self)} service failed to complete the embedding request.",
ex,
) from ex
if isinstance(embedding_response, EmbeddingResponse):
return [item.embedding for item in embedding_response.data]
return []
@@ -0,0 +1,32 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import ClassVar
from pydantic import SecretStr
from semantic_kernel.kernel_pydantic import KernelBaseSettings
class MistralAISettings(KernelBaseSettings):
"""MistralAI model settings.
The settings are first loaded from environment variables with the prefix 'MISTRALAI_'. 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.
Optional settings for prefix 'MISTRALAI_' are:
- api_key: SecretStr - MISTRAL API key, see https://console.mistral.ai/api-keys
(Env var MISTRALAI_API_KEY)
- chat_model_id: str | None - The The Mistral AI chat model ID to use see https://docs.mistral.ai/getting-started/models/.
(Env var MISTRALAI_CHAT_MODEL_ID)
- embedding_model_id: str | None - The The Mistral AI embedding model ID to use see https://docs.mistral.ai/getting-started/models/.
(Env var MISTRALAI_EMBEDDING_MODEL_ID)
- env_file_path: str | None - if provided, the .env settings are read from this file path location
"""
env_prefix: ClassVar[str] = "MISTRALAI_"
api_key: SecretStr
chat_model_id: str | None = None
embedding_model_id: str | None = None
@@ -0,0 +1,66 @@
# semantic_kernel.connectors.ai.nvidia
This connector enables integration with NVIDIA NIM API for text embeddings and chat completion. It allows you to use NVIDIA's models within the Semantic Kernel framework.
## Quick start
### Initialize the kernel
```python
import semantic_kernel as sk
kernel = sk.Kernel()
```
### Add NVIDIA text embedding service
You can provide your API key directly or through environment variables
```python
from semantic_kernel.connectors.ai.nvidia import NvidiaTextEmbedding
embedding_service = NvidiaTextEmbedding(
ai_model_id="nvidia/nv-embedqa-e5-v5", # Default model if not specified
api_key="your-nvidia-api-key", # Can also use NVIDIA_API_KEY env variable
service_id="nvidia-embeddings" # Optional service identifier
)
```
### Add the embedding service to the kernel
```python
kernel.add_service(embedding_service)
```
### Generate embeddings for text
```python
texts = ["Hello, world!", "Semantic Kernel is awesome"]
embeddings = await kernel.get_service("nvidia-embeddings").generate_embeddings(texts)
```
### Add NVIDIA chat completion service
```python
from semantic_kernel.connectors.ai.nvidia import NvidiaChatCompletion
chat_service = NvidiaChatCompletion(
ai_model_id="meta/llama-3.1-8b-instruct", # Default model if not specified
api_key="your-nvidia-api-key", # Can also use NVIDIA_API_KEY env variable
service_id="nvidia-chat" # Optional service identifier
)
kernel.add_service(chat_service)
```
### Basic chat completion
```python
response = await kernel.invoke_prompt("Hello, how are you?")
```
### Using with Chat Completion Agent
```python
from semantic_kernel.agents import ChatCompletionAgent
from semantic_kernel.connectors.ai.nvidia import NvidiaChatCompletion
agent = ChatCompletionAgent(
service=NvidiaChatCompletion(),
name="SK-Assistant",
instructions="You are a helpful assistant.",
)
response = await agent.get_response(messages="Write a haiku about Semantic Kernel.")
print(response.content)
```
@@ -0,0 +1,19 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.nvidia.prompt_execution_settings.nvidia_prompt_execution_settings import (
NvidiaChatPromptExecutionSettings,
NvidiaEmbeddingPromptExecutionSettings,
NvidiaPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.nvidia.services.nvidia_chat_completion import NvidiaChatCompletion
from semantic_kernel.connectors.ai.nvidia.services.nvidia_text_embedding import NvidiaTextEmbedding
from semantic_kernel.connectors.ai.nvidia.settings.nvidia_settings import NvidiaSettings
__all__ = [
"NvidiaChatCompletion",
"NvidiaChatPromptExecutionSettings",
"NvidiaEmbeddingPromptExecutionSettings",
"NvidiaPromptExecutionSettings",
"NvidiaSettings",
"NvidiaTextEmbedding",
]
@@ -0,0 +1 @@
# Copyright (c) Microsoft. All rights reserved.
@@ -0,0 +1,73 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Annotated, Any, Literal
from pydantic import BaseModel, Field
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
class NvidiaPromptExecutionSettings(PromptExecutionSettings):
"""Settings for NVIDIA prompt execution."""
format: Literal["json"] | None = None
options: dict[str, Any] | None = None
class NvidiaEmbeddingPromptExecutionSettings(NvidiaPromptExecutionSettings):
"""Settings for NVIDIA embedding prompt execution."""
input: str | list[str] | None = None
ai_model_id: Annotated[str | None, Field(serialization_alias="model")] = None
encoding_format: Literal["float", "base64"] = "float"
truncate: Literal["NONE", "START", "END"] = "NONE"
input_type: Literal["passage", "query"] = "query" # required param with default value query
user: str | None = None
extra_headers: dict | None = None
extra_body: dict | None = None
timeout: float | None = None
dimensions: Annotated[int | None, Field(gt=0)] = None
def prepare_settings_dict(self, **kwargs) -> dict[str, Any]:
"""Override only for embeddings to exclude input_type and truncate."""
return self.model_dump(
exclude={"service_id", "extension_data", "structured_json_response", "input_type", "truncate"},
exclude_none=True,
by_alias=True,
)
class NvidiaChatPromptExecutionSettings(NvidiaPromptExecutionSettings):
"""Settings for NVIDIA chat prompt execution."""
messages: list[dict[str, str]] | None = None
ai_model_id: Annotated[str | None, Field(serialization_alias="model")] = None
temperature: float | None = None
top_p: float | None = None
n: int | None = None
stream: bool = False
stop: str | list[str] | None = None
max_tokens: int | None = None
presence_penalty: float | None = None
frequency_penalty: float | None = None
logit_bias: dict[str, float] | None = None
user: str | None = None
tools: list[dict[str, Any]] | None = None
tool_choice: str | dict[str, Any] | None = None
response_format: (
dict[Literal["type"], Literal["text", "json_object"]] | dict[str, Any] | type[BaseModel] | type | None
) = None
seed: int | None = None
extra_headers: dict | None = None
extra_body: dict | None = None
timeout: float | None = None
# NVIDIA-specific structured output support
nvext: dict[str, Any] | None = None
def prepare_settings_dict(self, **kwargs) -> dict[str, Any]:
"""Override only for embeddings to exclude input_type and truncate."""
return self.model_dump(
exclude={"service_id", "extension_data", "structured_json_response", "response_format"},
exclude_none=True,
by_alias=True,
)
@@ -0,0 +1 @@
# Copyright (c) Microsoft. All rights reserved.
@@ -0,0 +1,313 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
import sys
from collections.abc import AsyncGenerator
from typing import Any, Literal
from openai import AsyncOpenAI
from openai.types.chat.chat_completion import ChatCompletion, Choice
from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice
from pydantic import ValidationError
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.nvidia.prompt_execution_settings.nvidia_prompt_execution_settings import (
NvidiaChatPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.nvidia.services.nvidia_handler import NvidiaHandler
from semantic_kernel.connectors.ai.nvidia.services.nvidia_model_types import NvidiaModelTypes
from semantic_kernel.connectors.ai.nvidia.settings.nvidia_settings import NvidiaSettings
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.contents import (
AuthorRole,
ChatMessageContent,
FinishReason,
FunctionCallContent,
StreamingChatMessageContent,
StreamingTextContent,
TextContent,
)
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
from semantic_kernel.utils.feature_stage_decorator import experimental
from semantic_kernel.utils.telemetry.model_diagnostics.decorators import (
trace_chat_completion,
trace_streaming_chat_completion,
)
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__)
# Default NVIDIA chat model when none is specified
DEFAULT_NVIDIA_CHAT_MODEL = "meta/llama-3.1-8b-instruct"
@experimental
class NvidiaChatCompletion(NvidiaHandler, ChatCompletionClientBase):
"""NVIDIA Chat completion class.
This class does not support function calling. The SUPPORTS_FUNCTION_CALLING attribute
is set to False (inherited from the base class).
"""
def __init__(
self,
ai_model_id: str | None = None,
api_key: str | None = None,
base_url: str | None = None,
service_id: str | None = None,
client: AsyncOpenAI | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
instruction_role: Literal["system", "user", "assistant", "developer"] | None = None,
) -> None:
"""Initialize an NvidiaChatCompletion service.
Args:
ai_model_id (str): NVIDIA model name, see
https://docs.api.nvidia.com/nim/reference/
If not provided, defaults to DEFAULT_NVIDIA_CHAT_MODEL.
service_id (str | None): Service ID tied to the execution settings.
api_key (str | None): The optional API key to use. If provided will override,
the env vars or .env file value.
base_url (str | None): Custom API endpoint. (Optional)
client (Optional[AsyncOpenAI]): An existing client to use. (Optional)
env_file_path (str | None): Use the environment settings file as a fallback
to environment variables. (Optional)
env_file_encoding (str | None): The encoding of the environment settings file. (Optional)
instruction_role (Literal["system", "user", "assistant", "developer"] | None): The role to use for
'instruction' messages. Defaults to "system". (Optional)
"""
try:
nvidia_settings = NvidiaSettings(
api_key=api_key,
base_url=base_url,
chat_model_id=ai_model_id,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as ex:
raise ServiceInitializationError("Failed to create NVIDIA settings.", ex) from ex
if not client and not nvidia_settings.api_key:
raise ServiceInitializationError("The NVIDIA API key is required.")
if not nvidia_settings.chat_model_id:
# Default fallback model
nvidia_settings.chat_model_id = DEFAULT_NVIDIA_CHAT_MODEL
logger.warning(f"Default chat model set as: {nvidia_settings.chat_model_id}")
# Create client if not provided
if not client:
client = AsyncOpenAI(
api_key=nvidia_settings.api_key.get_secret_value() if nvidia_settings.api_key else None,
base_url=nvidia_settings.base_url,
)
super().__init__(
ai_model_id=nvidia_settings.chat_model_id,
api_key=nvidia_settings.api_key.get_secret_value() if nvidia_settings.api_key else None,
base_url=nvidia_settings.base_url,
service_id=service_id or "",
ai_model_type=NvidiaModelTypes.CHAT,
client=client,
instruction_role=instruction_role or "system",
)
@classmethod
def from_dict(cls: type["NvidiaChatCompletion"], settings: dict[str, Any]) -> "NvidiaChatCompletion":
"""Initialize an NVIDIA service from a dictionary of settings.
Args:
settings: A dictionary of settings for the service.
"""
return cls(
ai_model_id=settings.get("ai_model_id"),
api_key=settings.get("api_key"),
base_url=settings.get("base_url"),
service_id=settings.get("service_id"),
env_file_path=settings.get("env_file_path"),
)
@override
def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]:
return NvidiaChatPromptExecutionSettings
@override
@trace_chat_completion("nvidia")
async def _inner_get_chat_message_contents(
self,
chat_history: "ChatHistory",
settings: "PromptExecutionSettings",
) -> list["ChatMessageContent"]:
if not isinstance(settings, NvidiaChatPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, NvidiaChatPromptExecutionSettings) # nosec
settings.stream = False
settings.messages = self._prepare_chat_history_for_request(chat_history)
settings.ai_model_id = settings.ai_model_id or self.ai_model_id
# Handle structured output
self._handle_structured_output(settings)
response = await self._send_request(settings)
assert isinstance(response, ChatCompletion) # nosec
response_metadata = self._get_metadata_from_chat_response(response)
return [self._create_chat_message_content(response, choice, response_metadata) for choice in response.choices]
@override
@trace_streaming_chat_completion("nvidia")
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, NvidiaChatPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, NvidiaChatPromptExecutionSettings) # nosec
settings.stream = True
settings.messages = self._prepare_chat_history_for_request(chat_history)
settings.ai_model_id = settings.ai_model_id or self.ai_model_id
# Handle structured output
self._handle_structured_output(settings)
response = await self._send_request(settings)
assert isinstance(response, AsyncGenerator) # nosec
async for chunk in response:
if len(chunk.choices) == 0:
continue
chunk_metadata = self._get_metadata_from_chat_response(chunk)
yield [
self._create_streaming_chat_message_content(chunk, choice, chunk_metadata, function_invoke_attempt)
for choice in chunk.choices
]
def _create_chat_message_content(
self, response: ChatCompletion, choice: Choice, response_metadata: dict[str, Any]
) -> "ChatMessageContent":
"""Create a chat message content object from a choice."""
metadata = self._get_metadata_from_chat_choice(choice)
metadata.update(response_metadata)
items: list[Any] = self._get_tool_calls_from_chat_choice(choice)
items.extend(self._get_function_call_from_chat_choice(choice))
if choice.message.content:
items.append(TextContent(text=choice.message.content))
return ChatMessageContent(
inner_content=response,
ai_model_id=self.ai_model_id,
metadata=metadata,
role=AuthorRole(choice.message.role),
items=items,
finish_reason=(FinishReason(choice.finish_reason) if choice.finish_reason else None),
)
def _create_streaming_chat_message_content(
self,
chunk: ChatCompletionChunk,
choice: ChunkChoice,
chunk_metadata: dict[str, Any],
function_invoke_attempt: int,
) -> StreamingChatMessageContent:
"""Create a streaming chat message content object from a choice."""
metadata = self._get_metadata_from_chat_choice(choice)
metadata.update(chunk_metadata)
items: list[Any] = self._get_tool_calls_from_chat_choice(choice)
items.extend(self._get_function_call_from_chat_choice(choice))
if choice.delta and choice.delta.content is not None:
items.append(StreamingTextContent(choice_index=choice.index, text=choice.delta.content))
return StreamingChatMessageContent(
choice_index=choice.index,
inner_content=chunk,
ai_model_id=self.ai_model_id,
metadata=metadata,
role=(AuthorRole(choice.delta.role) if choice.delta and choice.delta.role else AuthorRole.ASSISTANT),
finish_reason=(FinishReason(choice.finish_reason) if choice.finish_reason else None),
items=items,
function_invoke_attempt=function_invoke_attempt,
)
def _get_metadata_from_chat_response(self, response: ChatCompletion | ChatCompletionChunk) -> dict[str, Any]:
"""Get metadata from a chat response."""
return {
"id": response.id,
"created": response.created,
"system_fingerprint": getattr(response, "system_fingerprint", None),
"usage": CompletionUsage.from_openai(response.usage) if response.usage is not None else None,
}
def _get_metadata_from_chat_choice(self, choice: Choice | ChunkChoice) -> dict[str, Any]:
"""Get metadata from a chat choice."""
return {
"logprobs": getattr(choice, "logprobs", None),
}
def _get_tool_calls_from_chat_choice(self, choice: Choice | ChunkChoice) -> list[FunctionCallContent]:
"""Get tool calls from a chat choice."""
content = choice.message if isinstance(choice, Choice) else choice.delta
if content and (tool_calls := getattr(content, "tool_calls", None)) is not None:
return [
FunctionCallContent(
id=tool.id,
index=getattr(tool, "index", None),
name=tool.function.name,
arguments=tool.function.arguments,
)
for tool in tool_calls
]
return []
def _get_function_call_from_chat_choice(self, choice: Choice | ChunkChoice) -> list[FunctionCallContent]:
"""Get function calls from a chat choice."""
content = choice.message if isinstance(choice, Choice) else choice.delta
if content and (function_call := getattr(content, "function_call", None)) is not None:
return [
FunctionCallContent(
id="",
name=function_call.name,
arguments=function_call.arguments,
)
]
return []
def _handle_structured_output(self, request_settings: NvidiaChatPromptExecutionSettings) -> None:
"""Handle structured output for NVIDIA models using nvext parameter."""
response_format = getattr(request_settings, "response_format", None)
if response_format:
# Convert Pydantic model to JSON schema for NVIDIA's guided_json
if hasattr(response_format, "model_json_schema"):
# It's a Pydantic model
schema = response_format.model_json_schema()
if not request_settings.extra_body:
request_settings.extra_body = {}
request_settings.extra_body["nvext"] = {"guided_json": schema}
elif isinstance(response_format, dict):
# It's already a dict, use it directly
if not request_settings.extra_body:
request_settings.extra_body = {}
request_settings.extra_body["nvext"] = {"guided_json": response_format}
def _prepare_chat_history_for_request(
self,
chat_history: ChatHistory,
role_key: str = "role",
content_key: str = "content",
) -> list[dict[str, str]]:
"""Prepare chat history for request."""
messages = []
for message in chat_history.messages:
message_dict = {role_key: message.role.value, content_key: message.content}
messages.append(message_dict)
return messages
@@ -0,0 +1,123 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
from abc import ABC
from typing import Any, ClassVar, Union
from openai import AsyncOpenAI, AsyncStream
from openai.types.chat.chat_completion import ChatCompletion
from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
from openai.types.completion import Completion
from openai.types.create_embedding_response import CreateEmbeddingResponse
from semantic_kernel.connectors.ai.nvidia.prompt_execution_settings.nvidia_prompt_execution_settings import (
NvidiaChatPromptExecutionSettings,
NvidiaEmbeddingPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.nvidia.services.nvidia_model_types import NvidiaModelTypes
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.const import USER_AGENT
from semantic_kernel.exceptions import ServiceResponseException
from semantic_kernel.kernel_pydantic import KernelBaseModel
logger: logging.Logger = logging.getLogger(__name__)
RESPONSE_TYPE = Union[list[Any], ChatCompletion, Completion, AsyncStream[Any]]
class NvidiaHandler(KernelBaseModel, ABC):
"""Internal class for calls to Nvidia API's."""
MODEL_PROVIDER_NAME: ClassVar[str] = "nvidia"
client: AsyncOpenAI
ai_model_type: NvidiaModelTypes = NvidiaModelTypes.CHAT
completion_tokens: int = 0
total_tokens: int = 0
prompt_tokens: int = 0
async def _send_request(self, settings: PromptExecutionSettings) -> RESPONSE_TYPE:
"""Send a request to the Nvidia API."""
if self.ai_model_type == NvidiaModelTypes.EMBEDDING:
assert isinstance(settings, NvidiaEmbeddingPromptExecutionSettings) # nosec
return await self._send_embedding_request(settings)
if self.ai_model_type == NvidiaModelTypes.CHAT:
assert isinstance(settings, NvidiaChatPromptExecutionSettings) # nosec
return await self._send_chat_completion_request(settings)
raise NotImplementedError(f"Model type {self.ai_model_type} is not supported")
async def _send_embedding_request(self, settings: NvidiaEmbeddingPromptExecutionSettings) -> list[Any]:
"""Send a request to the OpenAI embeddings endpoint."""
try:
# unsupported parameters are internally excluded from main dict and added to extra_body
response = await self.client.embeddings.create(**settings.prepare_settings_dict())
self.store_usage(response)
return [x.embedding for x in response.data]
except Exception as ex:
raise ServiceResponseException(
f"{type(self)} service failed to generate embeddings",
ex,
) from ex
async def _send_chat_completion_request(
self, settings: NvidiaChatPromptExecutionSettings
) -> ChatCompletion | AsyncStream[Any]:
"""Send a request to the NVIDIA chat completion endpoint."""
try:
settings_dict = settings.prepare_settings_dict()
# Handle structured output if nvext is present in extra_body
if settings.extra_body and "nvext" in settings.extra_body:
if "extra_body" not in settings_dict:
settings_dict["extra_body"] = {}
settings_dict["extra_body"]["nvext"] = settings.extra_body["nvext"]
response = await self.client.chat.completions.create(**settings_dict)
self.store_usage(response)
return response
except Exception as ex:
raise ServiceResponseException(
f"{type(self)} service failed to complete the chat",
ex,
) from ex
def store_usage(
self,
response: ChatCompletion
| Completion
| AsyncStream[ChatCompletionChunk]
| AsyncStream[Completion]
| CreateEmbeddingResponse,
):
"""Store the usage information from the response."""
if not isinstance(response, AsyncStream) and response.usage:
logger.info(f"OpenAI usage: {response.usage}")
self.prompt_tokens += response.usage.prompt_tokens
self.total_tokens += response.usage.total_tokens
if hasattr(response.usage, "completion_tokens"):
self.completion_tokens += response.usage.completion_tokens
def to_dict(self) -> dict[str, str]:
"""Create a dict of the service settings."""
client_settings = {
"api_key": self.client.api_key,
"default_headers": {k: v for k, v in self.client.default_headers.items() if k != USER_AGENT},
}
if self.client.organization:
client_settings["org_id"] = self.client.organization
base = self.model_dump(
exclude={
"prompt_tokens",
"completion_tokens",
"total_tokens",
"api_type",
"ai_model_type",
"service_id",
"client",
},
by_alias=True,
exclude_none=True,
)
base.update(client_settings)
return base
@@ -0,0 +1,10 @@
# Copyright (c) Microsoft. All rights reserved.
from enum import Enum
class NvidiaModelTypes(Enum):
"""Nvidia model types, can be text, chat, or embedding."""
EMBEDDING = "embedding"
CHAT = "chat"
@@ -0,0 +1,167 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import copy
import logging
import sys
from typing import Any
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
from numpy import array, ndarray
from openai import AsyncOpenAI
from pydantic import ValidationError
from semantic_kernel.connectors.ai.embedding_generator_base import EmbeddingGeneratorBase
from semantic_kernel.connectors.ai.nvidia.prompt_execution_settings.nvidia_prompt_execution_settings import (
NvidiaEmbeddingPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.nvidia.services.nvidia_handler import NvidiaHandler
from semantic_kernel.connectors.ai.nvidia.services.nvidia_model_types import NvidiaModelTypes
from semantic_kernel.connectors.ai.nvidia.settings.nvidia_settings import NvidiaSettings
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError
from semantic_kernel.utils.feature_stage_decorator import experimental
logger: logging.Logger = logging.getLogger(__name__)
@experimental
class NvidiaTextEmbedding(NvidiaHandler, EmbeddingGeneratorBase):
"""Nvidia text embedding service."""
def __init__(
self,
ai_model_id: str | None = None,
api_key: str | None = None,
base_url: str | None = None,
client: AsyncOpenAI | None = None,
env_file_path: str | None = None,
service_id: str | None = None,
) -> None:
"""Initializes a new instance of the NvidiaTextEmbedding class.
Args:
ai_model_id (str): NVIDIA model card string, see
https://Nvidia.co/sentence-transformers
api_key: NVIDIA API key, see https://console.NVIDIA.com/settings/keys
(Env var NVIDIA_API_KEY)
base_url: HttpsUrl | None - base_url: The url of the NVIDIA endpoint. The base_url consists of the endpoint,
and more information refer https://docs.api.nvidia.com/nim/reference/
use endpoint if you only want to supply the endpoint.
(Env var NVIDIA_BASE_URL)
client (Optional[AsyncOpenAI]): An existing client to use. (Optional)
env_file_path (str | None): Use the environment settings file as
a fallback to environment variables. (Optional)
service_id (str): Service ID for the model. (optional)
"""
try:
nvidia_settings = NvidiaSettings(
api_key=api_key,
base_url=base_url,
embedding_model_id=ai_model_id,
env_file_path=env_file_path,
)
except ValidationError as ex:
raise ServiceInitializationError("Failed to create NVIDIA settings.", ex) from ex
if not nvidia_settings.embedding_model_id:
nvidia_settings.embedding_model_id = "nvidia/nv-embedqa-e5-v5"
logger.warning(f"Default embedding model set as: {nvidia_settings.embedding_model_id}")
if not nvidia_settings.api_key:
logger.warning("API_KEY is missing, inference may fail.")
if not client:
client = AsyncOpenAI(
api_key=nvidia_settings.api_key.get_secret_value() if nvidia_settings.api_key else None,
base_url=nvidia_settings.base_url,
)
super().__init__(
ai_model_id=nvidia_settings.embedding_model_id,
api_key=nvidia_settings.api_key.get_secret_value() if nvidia_settings.api_key else None,
ai_model_type=NvidiaModelTypes.EMBEDDING,
service_id=service_id or nvidia_settings.embedding_model_id,
env_file_path=env_file_path,
client=client,
)
@override
async def generate_embeddings(
self,
texts: list[str],
settings: "PromptExecutionSettings | None" = None,
batch_size: int | None = None,
**kwargs: Any,
) -> ndarray:
raw_embeddings = await self.generate_raw_embeddings(texts, settings, batch_size, **kwargs)
return array(raw_embeddings)
@override
async def generate_raw_embeddings(
self,
texts: list[str],
settings: "PromptExecutionSettings | None" = None,
batch_size: int | None = None,
**kwargs: Any,
) -> Any:
"""Returns embeddings for the given texts in the unedited format.
Args:
texts (List[str]): The texts to generate embeddings for.
settings (NvidiaEmbeddingPromptExecutionSettings): The settings to use for the request.
batch_size (int): The batch size to use for the request.
kwargs (Dict[str, Any]): Additional arguments to pass to the request.
"""
if not settings:
settings = NvidiaEmbeddingPromptExecutionSettings(ai_model_id=self.ai_model_id)
else:
if not isinstance(settings, NvidiaEmbeddingPromptExecutionSettings):
settings = self.get_prompt_execution_settings_from_settings(settings)
assert isinstance(settings, NvidiaEmbeddingPromptExecutionSettings) # nosec
if settings.ai_model_id is None:
settings.ai_model_id = self.ai_model_id
for key, value in kwargs.items():
setattr(settings, key, value)
# move input_type and truncate to extra-body
if not settings.extra_body:
settings.extra_body = {}
settings.extra_body.setdefault("input_type", settings.input_type)
if settings.truncate is not None:
settings.extra_body.setdefault("truncate", settings.truncate)
raw_embeddings = []
tasks = []
batch_size = batch_size or len(texts)
for i in range(0, len(texts), batch_size):
batch = texts[i : i + batch_size]
batch_settings = copy.deepcopy(settings)
batch_settings.input = batch
tasks.append(self._send_request(settings=batch_settings))
results = await asyncio.gather(*tasks)
for raw_embedding in results:
assert isinstance(raw_embedding, list) # nosec
raw_embeddings.extend(raw_embedding)
return raw_embeddings
def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]:
"""Get the request settings class."""
return NvidiaEmbeddingPromptExecutionSettings
@classmethod
def from_dict(cls: type["NvidiaTextEmbedding"], settings: dict[str, Any]) -> "NvidiaTextEmbedding":
"""Initialize an Open AI service from a dictionary of settings.
Args:
settings: A dictionary of settings for the service.
"""
return cls(
ai_model_id=settings.get("ai_model_id"),
api_key=settings.get("api_key"),
env_file_path=settings.get("env_file_path"),
service_id=settings.get("service_id"),
)
@@ -0,0 +1 @@
# Copyright (c) Microsoft. All rights reserved.
@@ -0,0 +1,37 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import ClassVar
from pydantic import SecretStr
from semantic_kernel.kernel_pydantic import KernelBaseSettings
class NvidiaSettings(KernelBaseSettings):
"""Nvidia model settings.
The settings are first loaded from environment variables with the prefix 'NVIDIA_'. 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.
Optional settings for prefix 'NVIDIA_' are:
- api_key: NVIDIA API key, see https://console.NVIDIA.com/settings/keys
(Env var NVIDIA_API_KEY)
- base_url: HttpsUrl | None - base_url: The url of the NVIDIA endpoint. The base_url consists of the endpoint,
and more information refer https://docs.api.nvidia.com/nim/reference/
use endpoint if you only want to supply the endpoint.
(Env var NVIDIA_BASE_URL)
- embedding_model_id: str | None - The NVIDIA embedding model ID to use, for example, nvidia/nv-embed-v1.
(Env var NVIDIA_EMBEDDING_MODEL_ID)
- chat_model_id: str | None - The NVIDIA chat model ID to use.
(Env var NVIDIA_CHAT_MODEL_ID)
- env_file_path: if provided, the .env settings are read from this file path location
"""
env_prefix: ClassVar[str] = "NVIDIA_"
api_key: SecretStr | None = None
base_url: str = "https://integrate.api.nvidia.com/v1"
embedding_model_id: str | None = None
chat_model_id: str | None = None
@@ -0,0 +1,21 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.ollama.ollama_prompt_execution_settings import (
OllamaChatPromptExecutionSettings,
OllamaEmbeddingPromptExecutionSettings,
OllamaPromptExecutionSettings,
OllamaTextPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.ollama.services.ollama_chat_completion import OllamaChatCompletion
from semantic_kernel.connectors.ai.ollama.services.ollama_text_completion import OllamaTextCompletion
from semantic_kernel.connectors.ai.ollama.services.ollama_text_embedding import OllamaTextEmbedding
__all__ = [
"OllamaChatCompletion",
"OllamaChatPromptExecutionSettings",
"OllamaEmbeddingPromptExecutionSettings",
"OllamaPromptExecutionSettings",
"OllamaTextCompletion",
"OllamaTextEmbedding",
"OllamaTextPromptExecutionSettings",
]
@@ -0,0 +1,39 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Annotated, Any, Literal
from pydantic import Field
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
class OllamaPromptExecutionSettings(PromptExecutionSettings):
"""Settings for Ollama prompt execution."""
format: Literal["json"] | dict[str, Any] | None = None
options: dict[str, Any] | None = None
class OllamaTextPromptExecutionSettings(OllamaPromptExecutionSettings):
"""Settings for Ollama text prompt execution."""
system: str | None = None
template: str | None = None
context: str | None = None
raw: bool | None = None
class OllamaChatPromptExecutionSettings(OllamaPromptExecutionSettings):
"""Settings for Ollama chat prompt execution."""
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
class OllamaEmbeddingPromptExecutionSettings(OllamaPromptExecutionSettings):
"""Settings for Ollama embedding prompt execution."""
@@ -0,0 +1,33 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import ClassVar
from semantic_kernel.kernel_pydantic import KernelBaseSettings
class OllamaSettings(KernelBaseSettings):
"""Ollama settings.
The settings are first loaded from environment variables with
the prefix 'OLLAMA_'.
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 'OLLAMA' are:
- chat_model_id: str - The chat model ID. (Env var OLLAMA_CHAT_MODEL_ID)
- text_model_id: str - The text model ID. (Env var OLLAMA_TEXT_MODEL_ID)
- embedding_model_id: str - The embedding model ID. (Env var OLLAMA_EMBEDDING_MODEL_ID)
Optional settings for prefix 'OLLAMA' are:
- host: HttpsUrl - The endpoint of the Ollama service. (Env var OLLAMA_HOST)
"""
env_prefix: ClassVar[str] = "OLLAMA_"
chat_model_id: str | None = None
text_model_id: str | None = None
embedding_model_id: str | None = None
host: str | None = None
@@ -0,0 +1,20 @@
# Copyright (c) Microsoft. All rights reserved.
from abc import ABC
from typing import ClassVar
from ollama import AsyncClient
from semantic_kernel.kernel_pydantic import KernelBaseModel
class OllamaBase(KernelBaseModel, ABC):
"""Ollama service base.
Args:
client [AsyncClient]: An Ollama client to use for the service.
"""
MODEL_PROVIDER_NAME: ClassVar[str] = "ollama"
client: AsyncClient

Some files were not shown because too many files have changed in this diff Show More