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