chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user