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