chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -0,0 +1,320 @@
# Copyright (c) Microsoft. All rights reserved.
import json
from unittest.mock import Mock
import pytest
from semantic_kernel.connectors.ai.bedrock.services.model_provider.bedrock_model_provider import BedrockModelProvider
from semantic_kernel.contents.chat_history import ChatHistory
@pytest.fixture()
def model_id(request) -> str:
if hasattr(request, "param"):
return request.param
return "test_model_id"
@pytest.fixture()
def service_id() -> str:
return "test_service_id"
@pytest.fixture()
def chat_history() -> ChatHistory:
chat_history = ChatHistory(system_message="You are a helpful assistant.")
chat_history.add_user_message("Hello!")
chat_history.add_assistant_message("Hi! How can I help you today?")
chat_history.add_system_message("Be polite and respectful.")
chat_history.add_user_message("I need help with a task.")
return chat_history
@pytest.fixture()
def bedrock_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
"""Fixture to set environment variables for Amazon Bedrock AI connector unit tests."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {
"BEDROCK_TEXT_MODEL_ID": "env_test_text_model_id",
"BEDROCK_CHAT_MODEL_ID": "env_test_chat_model_id",
"BEDROCK_EMBEDDING_MODEL_ID": "env_test_embedding_model_id",
"BEDROCK_MODEL_PROVIDER": "amazon",
}
env_vars.update(override_env_param_dict)
for key, value in env_vars.items():
if key not in exclude_list:
monkeypatch.setenv(key, value)
else:
monkeypatch.delenv(key, raising=False)
return env_vars
class MockBedrockClient(Mock):
def __init__(self, *args, **kwargs):
pass
def get_foundation_model(self, *args, **kwargs):
return {
"modelDetails": {
"responseStreamingSupported": True,
"inputModalities": ["TEXT"],
"outputModalities": ["TEXT", "EMBEDDING"],
}
}
class MockBedrockRuntimeClient(Mock):
def __init__(self, *args, **kwargs):
pass
def converse(self, *args, **kwargs):
pass
def converse_stream(self, *args, **kwargs):
pass
def invoke_model(self, *args, **kwargs):
pass
def invoke_model_with_response_stream(self, *args, **kwargs):
pass
# region mock chat completion responses
@pytest.fixture()
def mock_bedrock_chat_completion_response():
# https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-call.html#conversation-inference-call-response
return {
"output": {
"message": {
"role": "assistant",
"content": [
{
"text": "Hi! How can I help you today?",
}
],
}
},
"stopReason": "end_turn",
"usage": {
"inputTokens": 125,
"outputTokens": 60,
"totalTokens": 185,
},
}
@pytest.fixture()
def mock_bedrock_streaming_chat_completion_response():
# https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-call.html#conversation-inference-call-response
events = [
{"messageStart": {"role": "assistant"}},
{"contentBlockStart": {"contentBlockIndex": 0, "start": {}}},
{"contentBlockDelta": {"contentBlockIndex": 0, "delta": {"text": "Hi! "}}},
{"contentBlockDelta": {"contentBlockIndex": 0, "delta": {"text": "How can "}}},
{"contentBlockDelta": {"contentBlockIndex": 0, "delta": {"text": "I help you today?"}}},
{"contentBlockStop": {"contentBlockIndex": 0}},
{"messageStop": {"stopReason": "end_turn"}},
{
"metadata": {
"metrics": {"latencyMs": 1000},
"usage": {"inputTokens": 125, "outputTokens": 60, "totalTokens": 185},
}
},
]
def event_stream(events):
yield from events
return {"stream": event_stream(events)}
@pytest.fixture()
def mock_bedrock_streaming_chat_completion_invalid_response():
events = [
{"unknown": {}},
]
def event_stream(events):
yield from events
return {"stream": event_stream(events)}
# endregion
# region mock text completion responses
@pytest.fixture()
def output_text():
return "Hi! How can I help you today?"
@pytest.fixture()
def model_provider():
return BedrockModelProvider.AMAZON
@pytest.fixture()
def mock_bedrock_text_completion_response(
model_id: str,
output_text: str,
request,
):
# Check if model_provider fixture is requested by the test
model_provider = None
if "model_provider" in request.fixturenames:
model_provider = request.getfixturevalue("model_provider")
else:
model_provider = BedrockModelProvider.to_model_provider(model_id)
match model_provider:
case BedrockModelProvider.AMAZON:
body = {
"inputTextTokenCount": 10,
"results": [
{
"tokenCount": 10,
"outputText": output_text,
"completionReason": "FINISHED ",
}
],
}
case BedrockModelProvider.ANTHROPIC:
body = {
"completion": output_text,
"stop_reason": "stop_sequence",
"stop": "",
}
case BedrockModelProvider.COHERE:
body = {
"generations": [
{
"text": output_text,
}
],
}
case BedrockModelProvider.AI21LABS:
body = {
"completions": [
{
"data": {
"text": output_text,
}
}
],
}
case BedrockModelProvider.META:
body = {
"generation": output_text,
"prompt_token_count": 10,
"generation_token_count": 10,
}
case BedrockModelProvider.MISTRALAI:
body = {"outputs": [{"text": output_text}]}
mock = Mock()
mock.read.return_value = json.dumps(body)
return {"body": mock}
@pytest.fixture()
def mock_bedrock_streaming_text_completion_response(
model_id: str,
output_text: str,
request,
):
# Check if model_provider fixture is requested by the test
model_provider = None
if "model_provider" in request.fixturenames:
model_provider = request.getfixturevalue("model_provider")
else:
model_provider = BedrockModelProvider.to_model_provider(model_id)
match model_provider:
case BedrockModelProvider.AMAZON:
chunks = [
{
"chunk": {
"bytes": json.dumps({
"inputTextTokenCount": 10,
"totalOutputTextTokenCount": 10,
"outputText": chunk,
}).encode(),
}
}
for chunk in [output_text[i : i + 3] for i in range(0, len(output_text), 3)]
]
def event_stream(events):
yield from events
return {"body": event_stream(chunks)}
# endregion
# region mock text embedding responses
@pytest.fixture()
def mock_bedrock_text_embedding_response(
model_id: str,
request,
):
# Check if model_provider fixture is requested by the test
model_provider = None
if "model_provider" in request.fixturenames:
model_provider = request.getfixturevalue("model_provider")
else:
model_provider = BedrockModelProvider.to_model_provider(model_id)
match model_provider:
case BedrockModelProvider.AMAZON:
body = {
"embedding": [0.1, 0.2, 0.3],
}
case BedrockModelProvider.COHERE:
body = {
"embeddings": [[0.1, 0.2, 0.3]],
}
mock = Mock()
mock.read.return_value = json.dumps(body)
return {"body": mock}
@pytest.fixture()
def mock_bedrock_text_embedding_invalid_response(model_id: str):
model_provider = BedrockModelProvider.to_model_provider(model_id)
match model_provider:
case BedrockModelProvider.AMAZON:
body = {"embedding": 0.1}
case BedrockModelProvider.COHERE:
body = {"embeddings": 0.1}
mock = Mock()
mock.read.return_value = json.dumps(body)
return {"body": mock}
# endregion
@@ -0,0 +1,371 @@
# Copyright (c) Microsoft. All rights reserved.
from functools import reduce
from unittest.mock import Mock, patch
import boto3
import pytest
from semantic_kernel.connectors.ai.bedrock.bedrock_prompt_execution_settings import BedrockChatPromptExecutionSettings
from semantic_kernel.connectors.ai.bedrock.services.bedrock_chat_completion import BedrockChatCompletion
from semantic_kernel.connectors.ai.bedrock.services.model_provider.bedrock_model_provider import BedrockModelProvider
from semantic_kernel.connectors.ai.completion_usage import CompletionUsage
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.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 tests.unit.connectors.ai.bedrock.conftest import MockBedrockClient, MockBedrockRuntimeClient
# region init
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_chat_completion_init(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Chat Completion service"""
bedrock_chat_completion = BedrockChatCompletion()
assert bedrock_chat_completion.ai_model_id == bedrock_unit_test_env["BEDROCK_CHAT_MODEL_ID"]
assert bedrock_chat_completion.service_id == bedrock_unit_test_env["BEDROCK_CHAT_MODEL_ID"]
assert bedrock_chat_completion.bedrock_model_provider == BedrockModelProvider(
bedrock_unit_test_env["BEDROCK_MODEL_PROVIDER"]
)
assert bedrock_chat_completion.bedrock_client is not None
assert bedrock_chat_completion.bedrock_runtime_client is not None
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_chat_completion_init_model_id_override(mock_client, bedrock_unit_test_env, model_id) -> None:
"""Test initialization of Amazon Bedrock Chat Completion service"""
bedrock_chat_completion = BedrockChatCompletion(model_id=model_id)
assert bedrock_chat_completion.ai_model_id == model_id
assert bedrock_chat_completion.service_id == model_id
assert bedrock_chat_completion.bedrock_client is not None
assert bedrock_chat_completion.bedrock_runtime_client is not None
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_chat_completion_init_custom_service_id(mock_client, bedrock_unit_test_env, service_id) -> None:
"""Test initialization of Amazon Bedrock Chat Completion service"""
bedrock_chat_completion = BedrockChatCompletion(service_id=service_id)
assert bedrock_chat_completion.service_id == service_id
assert bedrock_chat_completion.bedrock_client is not None
assert bedrock_chat_completion.bedrock_runtime_client is not None
def test_bedrock_chat_completion_init_custom_clients(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Chat Completion service"""
bedrock_chat_completion = BedrockChatCompletion(
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
)
assert isinstance(bedrock_chat_completion.bedrock_client, MockBedrockClient)
assert isinstance(bedrock_chat_completion.bedrock_runtime_client, MockBedrockRuntimeClient)
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_chat_completion_init_custom_client(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Chat Completion service"""
bedrock_chat_completion = BedrockChatCompletion(
client=MockBedrockClient(),
)
assert isinstance(bedrock_chat_completion.bedrock_client, MockBedrockClient)
assert bedrock_chat_completion.bedrock_runtime_client is not None
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_chat_completion_init_custom_runtime_client(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Chat Completion service"""
bedrock_chat_completion = BedrockChatCompletion(
runtime_client=MockBedrockRuntimeClient(),
)
assert bedrock_chat_completion.bedrock_client is not None
assert isinstance(bedrock_chat_completion.bedrock_runtime_client, MockBedrockRuntimeClient)
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_chat_completion_init_custom_bedrock_model_provider(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Chat Completion service"""
bedrock_chat_completion = BedrockChatCompletion(
model_provider=BedrockModelProvider.AMAZON,
)
assert bedrock_chat_completion.bedrock_model_provider == BedrockModelProvider.AMAZON
@pytest.mark.parametrize("exclude_list", [["BEDROCK_CHAT_MODEL_ID"]], indirect=True)
def test_bedrock_chat_completion_client_init_with_empty_model_id(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Chat Completion service with empty model id"""
with pytest.raises(ServiceInitializationError, match="The Amazon Bedrock Chat Model ID is missing."):
BedrockChatCompletion(env_file_path="fake_env_file_path.env")
def test_bedrock_chat_completion_client_init_invalid_settings(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Chat Completion service with invalid settings"""
with pytest.raises(
ServiceInitializationError, match="Failed to initialize the Amazon Bedrock Chat Completion Service."
):
BedrockChatCompletion(model_id=123) # Model ID must be a string
def test_bedrock_chat_completion_client_init_invalid_model_provider(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Chat Completion service with invalid settings"""
with pytest.raises(
ServiceInitializationError, match="Failed to initialize the Amazon Bedrock Chat Completion Service."
):
BedrockChatCompletion(model_provider="invalid_provider")
@patch.object(boto3, "client", return_value=Mock())
def test_prompt_execution_settings_class(mock_client, bedrock_unit_test_env) -> None:
"""Test getting prompt execution settings class"""
bedrock_completion_client = BedrockChatCompletion()
assert bedrock_completion_client.get_prompt_execution_settings_class() == BedrockChatPromptExecutionSettings
# endregion
# region private methods
@patch.object(boto3, "client", return_value=Mock())
def test_prepare_chat_history_for_request(mock_client, bedrock_unit_test_env, chat_history) -> None:
"""Test preparing chat history for request"""
bedrock_chat_completion = BedrockChatCompletion()
parsed_chat_history = bedrock_chat_completion._prepare_chat_history_for_request(chat_history)
assert isinstance(parsed_chat_history, list)
assert len(parsed_chat_history) == len(chat_history) - 2 # Exclude system message
assert all([item["role"] in ["user", "assistant"] for item in parsed_chat_history])
@patch.object(boto3, "client", return_value=Mock())
def test_prepare_system_message_for_request(mock_client, bedrock_unit_test_env, chat_history) -> None:
"""Test preparing system message for request"""
bedrock_chat_completion = BedrockChatCompletion()
parsed_system_message = bedrock_chat_completion._prepare_system_messages_for_request(chat_history)
assert isinstance(parsed_system_message, list)
assert len(parsed_system_message) == 2
@pytest.mark.parametrize(
"model_id",
[
"amazon.titan",
"anthropic.claude",
"cohere.command",
"ai21.jamba",
"meta.llama",
"mistral.ai",
],
)
@patch.object(boto3, "client", return_value=Mock())
def test_prepare_settings_for_request(mock_client, model_id, chat_history) -> None:
"""Test preparing settings for request"""
bedrock_chat_completion = BedrockChatCompletion(model_id=model_id)
settings = BedrockChatPromptExecutionSettings()
parsed_settings = bedrock_chat_completion._prepare_settings_for_request(chat_history, settings)
assert isinstance(parsed_settings, dict)
assert parsed_settings["modelId"] == bedrock_chat_completion.ai_model_id
assert parsed_settings["messages"] == bedrock_chat_completion._prepare_chat_history_for_request(chat_history)
assert parsed_settings["system"] == bedrock_chat_completion._prepare_system_messages_for_request(chat_history)
assert isinstance(parsed_settings["inferenceConfig"], dict)
assert all([parsed_settings["inferenceConfig"].values()])
@pytest.mark.parametrize(
"model_id",
[
"arn:aws:bedrock:us-east-1:972143716085:application-inference-profile/123456",
],
)
@patch.object(boto3, "client", return_value=Mock())
def test_prepare_settings_for_request_with_application_inference_profile(mock_client, model_id, chat_history) -> None:
"""Test preparing settings for request"""
# Without a valid model provider, it should raise an error
bedrock_chat_completion = BedrockChatCompletion(model_id=model_id)
settings = BedrockChatPromptExecutionSettings()
with pytest.raises(
ValueError,
match=f"Model ID {model_id} does not contain a valid model provider name.",
):
bedrock_chat_completion._prepare_settings_for_request(chat_history, settings)
# With a valid model provider, it should not raise an error
bedrock_chat_completion = BedrockChatCompletion(model_id=model_id, model_provider=BedrockModelProvider.AMAZON)
parsed_settings = bedrock_chat_completion._prepare_settings_for_request(chat_history, settings)
assert isinstance(parsed_settings, dict)
assert parsed_settings["modelId"] == bedrock_chat_completion.ai_model_id
assert parsed_settings["messages"] == bedrock_chat_completion._prepare_chat_history_for_request(chat_history)
assert parsed_settings["system"] == bedrock_chat_completion._prepare_system_messages_for_request(chat_history)
assert isinstance(parsed_settings["inferenceConfig"], dict)
assert all([parsed_settings["inferenceConfig"].values()])
# endregion
# region chat completion
@pytest.mark.parametrize(
# These are fake model ids with the supported prefixes
"model_id",
[
"amazon.titan",
"anthropic.claude",
"cohere.command",
"ai21.jamba",
"meta.llama",
"mistral.ai",
],
)
async def test_bedrock_chat_completion(
model_id,
chat_history: ChatHistory,
mock_bedrock_chat_completion_response,
) -> None:
"""Test Amazon Bedrock Chat Completion complete method"""
with patch.object(
MockBedrockRuntimeClient, "converse", return_value=mock_bedrock_chat_completion_response
) as mock_converse:
# Setup
bedrock_chat_completion = BedrockChatCompletion(
model_id=model_id,
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
)
# Act
settings = BedrockChatPromptExecutionSettings()
response = await bedrock_chat_completion.get_chat_message_contents(chat_history=chat_history, settings=settings)
# Assert
mock_converse.assert_called_once_with(
**(bedrock_chat_completion._prepare_settings_for_request(chat_history, settings))
)
assert isinstance(response, list)
assert len(response) == 1
assert isinstance(response[0], ChatMessageContent)
assert response[0].ai_model_id == model_id
assert response[0].role == AuthorRole.ASSISTANT
assert len(response[0].items) == 1
assert isinstance(response[0].items[0], TextContent)
assert response[0].finish_reason == FinishReason.STOP
assert response[0].metadata["usage"] == CompletionUsage(
prompt_tokens=mock_bedrock_chat_completion_response["usage"]["inputTokens"],
completion_tokens=mock_bedrock_chat_completion_response["usage"]["outputTokens"],
)
assert (
response[0].items[0].text
== mock_bedrock_chat_completion_response["output"]["message"]["content"][0]["text"]
)
assert response[0].inner_content == mock_bedrock_chat_completion_response
@pytest.mark.parametrize(
# These are fake model ids with the supported prefixes
"model_id",
[
"amazon.titan",
"anthropic.claude",
"cohere.command",
"ai21.jamba",
"meta.llama",
"mistral.ai",
],
)
async def test_bedrock_streaming_chat_completion(
model_id,
chat_history: ChatHistory,
mock_bedrock_streaming_chat_completion_response,
) -> None:
"""Test Amazon Bedrock Streaming Chat Completion complete method"""
with patch.object(
MockBedrockRuntimeClient, "converse_stream", return_value=mock_bedrock_streaming_chat_completion_response
) as mock_converse_stream:
# Setup
bedrock_chat_completion = BedrockChatCompletion(
model_id=model_id,
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
)
# Act
settings = BedrockChatPromptExecutionSettings()
chunks: list[StreamingChatMessageContent] = []
async for streaming_messages in bedrock_chat_completion.get_streaming_chat_message_contents(
chat_history=chat_history, settings=settings
):
chunks.extend(streaming_messages)
response = reduce(lambda p, r: p + r, chunks)
# Assert
mock_converse_stream.assert_called_once_with(
**(bedrock_chat_completion._prepare_settings_for_request(chat_history, settings))
)
assert isinstance(response, StreamingChatMessageContent)
assert response.ai_model_id == model_id
assert response.role == AuthorRole.ASSISTANT
assert len(response.items) == 1
assert isinstance(response.inner_content, list)
assert len(response.inner_content) == 7
assert response.finish_reason == FinishReason.STOP
@pytest.mark.parametrize(
# These are fake model ids with the supported prefixes
"model_id",
[
"amazon.titan",
"anthropic.claude",
"cohere.command",
"ai21.jamba",
"meta.llama",
"mistral.ai",
],
)
async def test_bedrock_streaming_chat_completion_invalid_event(
model_id,
chat_history: ChatHistory,
mock_bedrock_streaming_chat_completion_invalid_response,
) -> None:
"""Test Amazon Bedrock Streaming Chat Completion complete method"""
with patch.object(
MockBedrockRuntimeClient,
"converse_stream",
return_value=mock_bedrock_streaming_chat_completion_invalid_response,
):
# Setup
bedrock_chat_completion = BedrockChatCompletion(
model_id=model_id,
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
)
# Act
settings = BedrockChatPromptExecutionSettings()
with pytest.raises(ServiceInvalidResponseError):
async for chunk in bedrock_chat_completion.get_streaming_chat_message_contents(
chat_history=chat_history, settings=settings
):
pass
# endregion
@@ -0,0 +1,443 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import MagicMock
import pytest
from semantic_kernel.connectors.ai.bedrock.bedrock_prompt_execution_settings import BedrockChatPromptExecutionSettings
from semantic_kernel.connectors.ai.bedrock.services.model_provider.bedrock_model_provider import (
BedrockModelProvider,
)
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.function_call_choice_configuration import FunctionCallChoiceConfiguration
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.connectors.ai.function_choice_type 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
from semantic_kernel.kernel import Kernel
def test_remove_none_recursively():
data = {
"a": 1,
"b": None,
"c": {
"d": 2,
"e": None,
"f": {
"g": 3,
"h": None,
},
},
}
expected = {
"a": 1,
"c": {
"d": 2,
"f": {
"g": 3,
},
},
}
assert remove_none_recursively(data) == expected
def test_remove_recursively_max_depth():
data = {
"a": {"b": None},
}
assert remove_none_recursively(data, max_depth=1) == data
def test_update_settings_from_function_choice_configuration_auto(kernel: Kernel, custom_plugin_class) -> None:
kernel.add_plugin(plugin=custom_plugin_class(), plugin_name="custom_plugin")
settings = BedrockChatPromptExecutionSettings()
auto_function_choice_behavior = FunctionChoiceBehavior.Auto()
auto_function_choice_behavior.configure(
kernel,
update_settings_from_function_choice_configuration,
settings,
)
assert "auto" in settings.tool_choice
assert len(settings.tools) == 1
def test_update_settings_from_function_choice_configuration_auto_without_plugin(kernel: Kernel) -> None:
settings = BedrockChatPromptExecutionSettings()
auto_function_choice_behavior = FunctionChoiceBehavior.Auto()
auto_function_choice_behavior.configure(
kernel,
update_settings_from_function_choice_configuration,
settings,
)
assert settings.tool_choice is None
assert settings.tools is None
def test_update_settings_from_function_choice_configuration_none(kernel: Kernel) -> None:
settings = BedrockChatPromptExecutionSettings()
auto_function_choice_behavior = FunctionChoiceBehavior.NoneInvoke()
auto_function_choice_behavior.configure(
kernel,
update_settings_from_function_choice_configuration,
settings,
)
assert settings.tool_choice is None
assert settings.tools is None
def test_update_settings_from_function_choice_configuration_required_with_one_function(
kernel: Kernel,
custom_plugin_class,
) -> None:
kernel.add_plugin(plugin=custom_plugin_class(), plugin_name="custom_plugin")
settings = BedrockChatPromptExecutionSettings()
auto_function_choice_behavior = FunctionChoiceBehavior.Required()
auto_function_choice_behavior.configure(
kernel,
update_settings_from_function_choice_configuration,
settings,
)
assert "tool" in settings.tool_choice
assert len(settings.tools) == 1
def test_update_settings_from_function_choice_configuration_required_with_more_than_one_functions(
kernel: Kernel,
custom_plugin_class,
experimental_plugin_class,
) -> None:
kernel.add_plugin(plugin=custom_plugin_class(), plugin_name="custom_plugin")
kernel.add_plugin(plugin=experimental_plugin_class(), plugin_name="experimental_plugin")
settings = BedrockChatPromptExecutionSettings()
auto_function_choice_behavior = FunctionChoiceBehavior.Required()
auto_function_choice_behavior.configure(
kernel,
update_settings_from_function_choice_configuration,
settings,
)
assert "any" in settings.tool_choice
assert len(settings.tools) == 2
def test_inference_profile_with_bedrock_model() -> None:
"""Test the BedrockModelProvider class returns the correct model for a given inference profile."""
us_amazon_inference_profile = "us.amazon.nova-lite-v1:0"
assert BedrockModelProvider.to_model_provider(us_amazon_inference_profile) == BedrockModelProvider.AMAZON
us_anthropic_inference_profile = "us.anthropic.claude-3-sonnet-20240229-v1:0"
assert BedrockModelProvider.to_model_provider(us_anthropic_inference_profile) == BedrockModelProvider.ANTHROPIC
eu_meta_inference_profile = "eu.meta.llama3-2-3b-instruct-v1:0"
assert BedrockModelProvider.to_model_provider(eu_meta_inference_profile) == BedrockModelProvider.META
unknown_inference_profile = "unknown"
with pytest.raises(ValueError, match="Model ID unknown does not contain a valid model provider name."):
BedrockModelProvider.to_model_provider(unknown_inference_profile)
def test_remove_none_recursively_empty_dict() -> None:
"""Test that an empty dict returns an empty dict."""
assert remove_none_recursively({}) == {}
def test_remove_none_recursively_no_none() -> None:
"""Test that a dict with no None values remains the same."""
original = {"a": 1, "b": 2}
result = remove_none_recursively(original)
assert result == {"a": 1, "b": 2}
def test_remove_none_recursively_with_none() -> None:
"""Test that dict values of None are removed."""
original = {"a": 1, "b": None, "c": {"d": None, "e": 3}}
result = remove_none_recursively(original)
# 'b' should be removed and 'd' inside nested dict should be removed
assert result == {"a": 1, "c": {"e": 3}}
def test_remove_none_recursively_max_depth() -> None:
"""Test that the function respects max_depth."""
original = {"a": {"b": {"c": None}}}
# If max_depth=1, it won't go deep enough to remove 'c'.
result = remove_none_recursively(original, max_depth=1)
assert result == {"a": {"b": {"c": None}}}
# If max_depth=3, it should remove 'c'.
result = remove_none_recursively(original, max_depth=3)
assert result == {"a": {"b": {}}}
def test_format_system_message() -> None:
"""Test that system message is formatted correctly."""
content = ChatMessageContent(role=AuthorRole.SYSTEM, content="System message")
formatted = MESSAGE_CONVERTERS[AuthorRole.SYSTEM](content)
assert formatted == {"text": "System message"}
def test_format_user_message_text_only() -> None:
"""Test user message with only text content."""
text_item = TextContent(text="Hello!")
user_message = ChatMessageContent(role=AuthorRole.USER, items=[text_item])
formatted = MESSAGE_CONVERTERS[AuthorRole.USER](user_message)
assert formatted["role"] == "user"
assert len(formatted["content"]) == 1
assert formatted["content"][0] == {"text": "Hello!"}
def test_format_user_message_image_only() -> None:
"""Test user message with only image content."""
img_item = ImageContent(data=b"abc", mime_type="image/png")
user_message = ChatMessageContent(role=AuthorRole.USER, items=[img_item])
formatted = MESSAGE_CONVERTERS[AuthorRole.USER](user_message)
assert formatted["role"] == "user"
assert len(formatted["content"]) == 1
image_section = formatted["content"][0].get("image")
assert image_section["format"] == "png"
assert image_section["source"]["bytes"] == b"abc"
def test_format_user_message_unsupported_content() -> None:
"""Test user message raises error with unsupported content type."""
# We can simulate an unsupported content type by using FunctionCallContent.
func_call_item = FunctionCallContent(id="123", function_name="test_function", arguments="{}")
user_message = ChatMessageContent(role=AuthorRole.USER, items=[func_call_item])
with pytest.raises(ServiceInvalidRequestError) as exc:
MESSAGE_CONVERTERS[AuthorRole.USER](user_message)
assert "Only text and image content are supported in a user message." in str(exc.value)
def test_format_assistant_message_text_content() -> None:
"""Test assistant message with text content."""
text_item = TextContent(text="Assistant response")
assistant_message = ChatMessageContent(role=AuthorRole.ASSISTANT, items=[text_item])
formatted = MESSAGE_CONVERTERS[AuthorRole.ASSISTANT](assistant_message)
assert formatted["role"] == "assistant"
assert formatted["content"] == [{"text": "Assistant response"}]
def test_format_assistant_message_function_call_content() -> None:
"""Test assistant message with function call content."""
func_item = FunctionCallContent(
id="fc1", plugin_name="plugin", function_name="function", arguments='{"param": "value"}'
)
assistant_message = ChatMessageContent(role=AuthorRole.ASSISTANT, items=[func_item])
formatted = MESSAGE_CONVERTERS[AuthorRole.ASSISTANT](assistant_message)
assert len(formatted["content"]) == 1
tool_use = formatted["content"][0].get("toolUse")
assert tool_use
assert tool_use["toolUseId"] == "fc1"
assert tool_use["name"] == "plugin-function"
assert tool_use["input"] == {"param": "value"}
def test_format_assistant_message_image_content_raises() -> None:
"""Test assistant message with image raises error."""
img_item = ImageContent(data=b"abc", mime_type="image/jpeg")
assistant_message = ChatMessageContent(role=AuthorRole.ASSISTANT, items=[img_item])
with pytest.raises(ServiceInvalidRequestError) as exc:
MESSAGE_CONVERTERS[AuthorRole.ASSISTANT](assistant_message)
assert "Image content is not supported in an assistant message." in str(exc.value)
def test_format_assistant_message_unsupported_type() -> None:
"""Test assistant message with unsupported item content type."""
func_res_item = FunctionResultContent(id="res1", function_name="some_function", result="some_result")
assistant_message = ChatMessageContent(role=AuthorRole.ASSISTANT, items=[func_res_item])
with pytest.raises(ServiceInvalidRequestError) as exc:
MESSAGE_CONVERTERS[AuthorRole.ASSISTANT](assistant_message)
assert "Unsupported content type in an assistant message:" in str(exc.value)
def test_format_tool_message_text() -> None:
"""Test tool message with text content."""
text_item = TextContent(text="Some text")
tool_message = ChatMessageContent(role=AuthorRole.TOOL, items=[text_item])
formatted = MESSAGE_CONVERTERS[AuthorRole.TOOL](tool_message)
assert formatted["role"] == "user" # note that for a tool message, role set to 'user'
assert formatted["content"] == [{"text": "Some text"}]
def test_format_tool_message_function_result() -> None:
"""Test tool message with function result content."""
func_result_item = FunctionResultContent(id="res_id", function_name="test_function", result="some result")
tool_message = ChatMessageContent(role=AuthorRole.TOOL, items=[func_result_item])
formatted = MESSAGE_CONVERTERS[AuthorRole.TOOL](tool_message)
assert formatted["role"] == "user"
content = formatted["content"][0]
assert content.get("toolResult")
assert content["toolResult"]["toolUseId"] == "res_id"
assert content["toolResult"]["content"] == [{"text": "some result"}]
def test_format_tool_message_image_raises() -> None:
"""Test tool message with image content raises an error."""
img_item = ImageContent(data=b"xyz", mime_type="image/jpeg")
tool_message = ChatMessageContent(role=AuthorRole.TOOL, items=[img_item])
with pytest.raises(ServiceInvalidRequestError) as exc:
MESSAGE_CONVERTERS[AuthorRole.TOOL](tool_message)
assert "Image content is not supported in a tool message." in str(exc.value)
def test_finish_reason_from_bedrock_to_semantic_kernel_stop() -> None:
"""Test that 'stop_sequence' maps to FinishReason.STOP"""
reason = finish_reason_from_bedrock_to_semantic_kernel("stop_sequence")
assert reason == FinishReason.STOP
reason = finish_reason_from_bedrock_to_semantic_kernel("end_turn")
assert reason == FinishReason.STOP
def test_finish_reason_from_bedrock_to_semantic_kernel_length() -> None:
"""Test that 'max_tokens' maps to FinishReason.LENGTH"""
reason = finish_reason_from_bedrock_to_semantic_kernel("max_tokens")
assert reason == FinishReason.LENGTH
def test_finish_reason_from_bedrock_to_semantic_kernel_content_filtered() -> None:
"""Test that 'content_filtered' maps to FinishReason.CONTENT_FILTER"""
reason = finish_reason_from_bedrock_to_semantic_kernel("content_filtered")
assert reason == FinishReason.CONTENT_FILTER
def test_finish_reason_from_bedrock_to_semantic_kernel_tool_use() -> None:
"""Test that 'tool_use' maps to FinishReason.TOOL_CALLS"""
reason = finish_reason_from_bedrock_to_semantic_kernel("tool_use")
assert reason == FinishReason.TOOL_CALLS
def test_finish_reason_from_bedrock_to_semantic_kernel_unknown() -> None:
"""Test that unknown finish reason returns None"""
reason = finish_reason_from_bedrock_to_semantic_kernel("something_unknown")
assert reason is None
@pytest.fixture
def mock_bedrock_settings() -> BedrockChatPromptExecutionSettings:
"""Helper fixture for BedrockChatPromptExecutionSettings."""
return BedrockChatPromptExecutionSettings()
@pytest.fixture
def mock_function_choice_config() -> FunctionCallChoiceConfiguration:
"""Helper fixture for a sample FunctionCallChoiceConfiguration."""
# We'll create mock kernel functions with metadata
mock_func_1 = MagicMock()
mock_func_1.fully_qualified_name = "plugin-function1"
mock_func_1.description = "Function 1 description"
param1 = MagicMock()
param1.name = "param1"
param1.schema_data = {"type": "string"}
param1.is_required = True
param2 = MagicMock()
param2.name = "param2"
param2.schema_data = {"type": "integer"}
param2.is_required = False
mock_func_1.parameters = [
param1,
param2,
]
mock_func_2 = MagicMock()
mock_func_2.fully_qualified_name = "plugin-function2"
mock_func_2.description = "Function 2 description"
mock_func_2.parameters = []
config = FunctionCallChoiceConfiguration()
config.available_functions = [mock_func_1, mock_func_2]
return config
def test_update_settings_from_function_choice_configuration_none_type(
mock_function_choice_config, mock_bedrock_settings
) -> None:
"""Test that if the FunctionChoiceType is NONE it doesn't modify settings."""
update_settings_from_function_choice_configuration(
mock_function_choice_config, mock_bedrock_settings, FunctionChoiceType.NONE
)
assert mock_bedrock_settings.tool_choice is None
assert mock_bedrock_settings.tools is None
def test_update_settings_from_function_choice_configuration_auto_two_tools(
mock_function_choice_config, mock_bedrock_settings
) -> None:
"""Test that AUTO sets tool_choice to {"auto": {}} and sets tools list"""
update_settings_from_function_choice_configuration(
mock_function_choice_config, mock_bedrock_settings, FunctionChoiceType.AUTO
)
assert mock_bedrock_settings.tool_choice == {"auto": {}}
assert len(mock_bedrock_settings.tools) == 2
# Validate structure of first tool
tool_spec_1 = mock_bedrock_settings.tools[0].get("toolSpec")
assert tool_spec_1["name"] == "plugin-function1"
assert tool_spec_1["description"] == "Function 1 description"
def test_update_settings_from_function_choice_configuration_required_many(
mock_function_choice_config, mock_bedrock_settings
) -> None:
"""Test that REQUIRED with more than one function sets tool_choice to {"any": {}}."""
update_settings_from_function_choice_configuration(
mock_function_choice_config, mock_bedrock_settings, FunctionChoiceType.REQUIRED
)
assert mock_bedrock_settings.tool_choice == {"any": {}}
assert len(mock_bedrock_settings.tools) == 2
def test_update_settings_from_function_choice_configuration_required_one(mock_bedrock_settings) -> None:
"""Test that REQUIRED with a single function picks "tool" with that function name."""
single_func = MagicMock()
single_func.fully_qualified_name = "plugin-function"
single_func.description = "Only function"
single_func.parameters = []
config = FunctionCallChoiceConfiguration()
config.available_functions = [single_func]
update_settings_from_function_choice_configuration(config, mock_bedrock_settings, FunctionChoiceType.REQUIRED)
assert mock_bedrock_settings.tool_choice == {"tool": {"name": "plugin-function"}}
assert len(mock_bedrock_settings.tools) == 1
assert mock_bedrock_settings.tools[0]["toolSpec"]["name"] == "plugin-function"
@@ -0,0 +1,324 @@
# Copyright (c) Microsoft. All rights reserved.
import json
from functools import reduce
from unittest.mock import Mock, patch
import boto3
import pytest
from semantic_kernel.connectors.ai.bedrock.bedrock_prompt_execution_settings import BedrockTextPromptExecutionSettings
from semantic_kernel.connectors.ai.bedrock.services.bedrock_text_completion import BedrockTextCompletion
from semantic_kernel.connectors.ai.bedrock.services.model_provider.bedrock_model_provider import (
BedrockModelProvider,
get_text_completion_request_body,
)
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 tests.unit.connectors.ai.bedrock.conftest import MockBedrockClient, MockBedrockRuntimeClient
# region init
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_completion_init(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Completion service"""
bedrock_text_completion = BedrockTextCompletion()
assert bedrock_text_completion.ai_model_id == bedrock_unit_test_env["BEDROCK_TEXT_MODEL_ID"]
assert bedrock_text_completion.service_id == bedrock_unit_test_env["BEDROCK_TEXT_MODEL_ID"]
assert bedrock_text_completion.bedrock_model_provider == BedrockModelProvider(
bedrock_unit_test_env["BEDROCK_MODEL_PROVIDER"]
)
assert bedrock_text_completion.bedrock_client is not None
assert bedrock_text_completion.bedrock_runtime_client is not None
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_completion_init_model_id_override(mock_client, bedrock_unit_test_env, model_id) -> None:
"""Test initialization of Amazon Bedrock Text Completion service"""
bedrock_text_completion = BedrockTextCompletion(model_id=model_id)
assert bedrock_text_completion.ai_model_id == model_id
assert bedrock_text_completion.service_id == model_id
assert bedrock_text_completion.bedrock_client is not None
assert bedrock_text_completion.bedrock_runtime_client is not None
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_completion_init_custom_service_id(mock_client, bedrock_unit_test_env, service_id) -> None:
"""Test initialization of Amazon Bedrock Text Completion service"""
bedrock_text_completion = BedrockTextCompletion(service_id=service_id)
assert bedrock_text_completion.service_id == service_id
assert bedrock_text_completion.bedrock_client is not None
assert bedrock_text_completion.bedrock_runtime_client is not None
def test_bedrock_text_completion_init_custom_clients(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Completion service"""
bedrock_text_completion = BedrockTextCompletion(
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
)
assert isinstance(bedrock_text_completion.bedrock_client, MockBedrockClient)
assert isinstance(bedrock_text_completion.bedrock_runtime_client, MockBedrockRuntimeClient)
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_completion_init_custom_client(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Completion service"""
bedrock_text_completion = BedrockTextCompletion(
client=MockBedrockClient(),
)
assert isinstance(bedrock_text_completion.bedrock_client, MockBedrockClient)
assert bedrock_text_completion.bedrock_runtime_client is not None
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_completion_init_custom_runtime_client(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Completion service"""
bedrock_text_completion = BedrockTextCompletion(
runtime_client=MockBedrockRuntimeClient(),
)
assert bedrock_text_completion.bedrock_client is not None
assert isinstance(bedrock_text_completion.bedrock_runtime_client, MockBedrockRuntimeClient)
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_completion_init_custom_bedrock_model_provider(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Completion service"""
bedrock_text_completion = BedrockTextCompletion(
model_provider=BedrockModelProvider.AMAZON,
)
assert bedrock_text_completion.bedrock_model_provider == BedrockModelProvider.AMAZON
@pytest.mark.parametrize("exclude_list", [["BEDROCK_TEXT_MODEL_ID"]], indirect=True)
def test_bedrock_text_completion_client_init_with_empty_model_id(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Completion service with empty model id"""
with pytest.raises(ServiceInitializationError, match="The Amazon Bedrock Text Model ID is missing."):
BedrockTextCompletion(env_file_path="fake_env_file_path.env")
def test_bedrock_text_completion_client_init_invalid_settings(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Completion service with invalid settings"""
with pytest.raises(
ServiceInitializationError, match="Failed to initialize the Amazon Bedrock Text Completion Service."
):
BedrockTextCompletion(model_id=123) # Model ID must be a string
def test_bedrock_text_completion_client_init_invalid_model_provider(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Completion service with invalid settings"""
with pytest.raises(
ServiceInitializationError, match="Failed to initialize the Amazon Bedrock Text Completion Service."
):
BedrockTextCompletion(model_provider="invalid_provider")
@patch.object(boto3, "client", return_value=Mock())
def test_prompt_execution_settings_class(mock_client, bedrock_unit_test_env) -> None:
"""Test getting prompt execution settings class"""
bedrock_completion_client = BedrockTextCompletion()
assert bedrock_completion_client.get_prompt_execution_settings_class() == BedrockTextPromptExecutionSettings
# endregion
# region text completion
@pytest.mark.parametrize(
# These are fake model ids with the supported prefixes
"model_id",
[
"amazon.titan",
"anthropic.claude",
"cohere.command",
"ai21.jamba",
"meta.llama",
"mistral.ai",
],
indirect=True,
)
async def test_bedrock_text_completion(
model_id,
mock_bedrock_text_completion_response,
output_text,
) -> None:
"""Test Amazon Bedrock Text Completion complete method"""
with patch.object(
MockBedrockRuntimeClient, "invoke_model", return_value=mock_bedrock_text_completion_response
) as mock_model_invoke:
# Setup
bedrock_text_completion = BedrockTextCompletion(
model_id=model_id,
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
)
# Act
settings = BedrockTextPromptExecutionSettings()
response = await bedrock_text_completion.get_text_contents("Hello!", settings=settings)
# Assert
mock_model_invoke.assert_called_once_with(
body=json.dumps(get_text_completion_request_body(model_id, "Hello!", settings)),
modelId=model_id,
accept="application/json",
contentType="application/json",
)
assert isinstance(response, list)
assert len(response) == 1
assert isinstance(response[0], TextContent)
assert response[0].ai_model_id == model_id
assert response[0].text == output_text
@pytest.mark.parametrize(
# These are fake model ids with the supported prefixes
"model_id",
[
"arn:aws:bedrock:us-east-1:972143716085:application-inference-profile/123456",
],
indirect=True,
)
async def test_bedrock_text_completion_with_application_inference_profile(
model_id,
mock_bedrock_text_completion_response,
output_text,
model_provider,
) -> None:
"""Test Amazon Bedrock Text Completion complete method"""
with patch.object(
MockBedrockRuntimeClient,
"invoke_model",
return_value=mock_bedrock_text_completion_response,
) as mock_model_invoke:
# Setup
bedrock_text_completion = BedrockTextCompletion(
model_id=model_id,
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
model_provider=model_provider,
)
# Act
settings = BedrockTextPromptExecutionSettings()
await bedrock_text_completion.get_text_contents("Hello!", settings=settings)
# Assert
mock_model_invoke.assert_called_once_with(
body=json.dumps(get_text_completion_request_body(model_id, "Hello!", settings, model_provider)),
modelId=model_id,
accept="application/json",
contentType="application/json",
)
@pytest.mark.parametrize(
# These are fake model ids with the supported prefixes
"model_id",
[
"amazon.titan",
],
indirect=True,
)
async def test_bedrock_streaming_text_completion(
model_id,
mock_bedrock_streaming_text_completion_response,
output_text,
) -> None:
"""Test Amazon Bedrock Text Completion complete method"""
with patch.object(
MockBedrockRuntimeClient,
"invoke_model_with_response_stream",
return_value=mock_bedrock_streaming_text_completion_response,
) as mock_invoke_model_with_response_stream:
# Setup
bedrock_text_completion = BedrockTextCompletion(
model_id=model_id,
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
)
# Act
settings = BedrockTextPromptExecutionSettings()
chunks: list[StreamingTextContent] = []
async for streaming_responses in bedrock_text_completion.get_streaming_text_contents(
"Hello!", settings=settings
):
chunks.extend(streaming_responses)
response = reduce(lambda p, r: p + r, chunks)
# Assert
mock_invoke_model_with_response_stream.assert_called_once_with(
body=json.dumps(get_text_completion_request_body(model_id, "Hello!", settings)),
modelId=model_id,
accept="application/json",
contentType="application/json",
)
assert isinstance(response, StreamingTextContent)
assert response.ai_model_id == model_id
assert response.text == output_text
assert response.choice_index == 0
assert isinstance(response.inner_content, list)
@pytest.mark.parametrize(
# These are fake model ids with the supported prefixes
"model_id",
[
"arn:aws:bedrock:us-east-1:972143716085:application-inference-profile/123456",
],
indirect=True,
)
async def test_bedrock_streaming_text_completion_with_application_inference_profile(
model_id,
mock_bedrock_streaming_text_completion_response,
output_text,
model_provider,
) -> None:
"""Test Amazon Bedrock Chat Completion complete method"""
with patch.object(
MockBedrockRuntimeClient,
"invoke_model_with_response_stream",
return_value=mock_bedrock_streaming_text_completion_response,
) as mock_invoke_model_with_response_stream:
# Setup
bedrock_text_completion = BedrockTextCompletion(
model_id=model_id,
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
model_provider=model_provider,
)
# Act
settings = BedrockTextPromptExecutionSettings()
chunks: list[StreamingTextContent] = []
async for streaming_responses in bedrock_text_completion.get_streaming_text_contents(
"Hello!", settings=settings
):
chunks.extend(streaming_responses)
# Assert
mock_invoke_model_with_response_stream.assert_called_once_with(
body=json.dumps(get_text_completion_request_body(model_id, "Hello!", settings, model_provider)),
modelId=model_id,
accept="application/json",
contentType="application/json",
)
# endregion
@@ -0,0 +1,235 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import ANY, Mock, patch
import boto3
import pytest
from semantic_kernel.connectors.ai.bedrock.bedrock_prompt_execution_settings import (
BedrockEmbeddingPromptExecutionSettings,
)
from semantic_kernel.connectors.ai.bedrock.services.bedrock_text_embedding import BedrockTextEmbedding
from semantic_kernel.connectors.ai.bedrock.services.model_provider.bedrock_model_provider import BedrockModelProvider
from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError, ServiceInvalidResponseError
from tests.unit.connectors.ai.bedrock.conftest import MockBedrockClient, MockBedrockRuntimeClient
# region init
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_embedding_init(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Embedding service"""
bedrock_text_embedding = BedrockTextEmbedding()
assert bedrock_text_embedding.ai_model_id == bedrock_unit_test_env["BEDROCK_EMBEDDING_MODEL_ID"]
assert bedrock_text_embedding.service_id == bedrock_unit_test_env["BEDROCK_EMBEDDING_MODEL_ID"]
assert bedrock_text_embedding.bedrock_model_provider == BedrockModelProvider(
bedrock_unit_test_env["BEDROCK_MODEL_PROVIDER"]
)
assert bedrock_text_embedding.bedrock_client is not None
assert bedrock_text_embedding.bedrock_runtime_client is not None
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_embedding_init_model_id_override(mock_client, bedrock_unit_test_env, model_id) -> None:
"""Test initialization of Amazon Bedrock Text Embedding service"""
bedrock_text_embedding = BedrockTextEmbedding(model_id=model_id)
assert bedrock_text_embedding.ai_model_id == model_id
assert bedrock_text_embedding.service_id == model_id
assert bedrock_text_embedding.bedrock_client is not None
assert bedrock_text_embedding.bedrock_runtime_client is not None
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_embedding_init_custom_service_id(mock_client, bedrock_unit_test_env, service_id) -> None:
"""Test initialization of Amazon Bedrock Text Embedding service"""
bedrock_text_embedding = BedrockTextEmbedding(service_id=service_id)
assert bedrock_text_embedding.service_id == service_id
assert bedrock_text_embedding.bedrock_client is not None
assert bedrock_text_embedding.bedrock_runtime_client is not None
def test_bedrock_text_embedding_init_custom_clients(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Embedding service"""
bedrock_text_embedding = BedrockTextEmbedding(
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
)
assert isinstance(bedrock_text_embedding.bedrock_client, MockBedrockClient)
assert isinstance(bedrock_text_embedding.bedrock_runtime_client, MockBedrockRuntimeClient)
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_embedding_init_custom_client(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Embedding service"""
bedrock_text_embedding = BedrockTextEmbedding(
client=MockBedrockClient(),
)
assert isinstance(bedrock_text_embedding.bedrock_client, MockBedrockClient)
assert bedrock_text_embedding.bedrock_runtime_client is not None
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_embedding_init_custom_runtime_client(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Embedding service"""
bedrock_text_embedding = BedrockTextEmbedding(
runtime_client=MockBedrockRuntimeClient(),
)
assert bedrock_text_embedding.bedrock_client is not None
assert isinstance(bedrock_text_embedding.bedrock_runtime_client, MockBedrockRuntimeClient)
@patch.object(boto3, "client", return_value=Mock())
def test_bedrock_text_embedding_init_custom_bedrock_model_provider(mock_client, bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Embedding service"""
bedrock_text_embedding = BedrockTextEmbedding(
model_provider=BedrockModelProvider.AMAZON,
)
assert bedrock_text_embedding.bedrock_model_provider == BedrockModelProvider.AMAZON
@pytest.mark.parametrize("exclude_list", [["BEDROCK_EMBEDDING_MODEL_ID"]], indirect=True)
def test_bedrock_text_embedding_client_init_with_empty_model_id(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Embedding service with empty model id"""
with pytest.raises(ServiceInitializationError, match="The Amazon Bedrock Text Embedding Model ID is missing."):
BedrockTextEmbedding(env_file_path="fake_env_file_path.env")
def test_bedrock_text_embedding_client_init_invalid_settings(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Embedding service with invalid settings"""
with pytest.raises(
ServiceInitializationError, match="Failed to initialize the Amazon Bedrock Text Embedding Service."
):
BedrockTextEmbedding(model_id=123) # Model ID must be a string
def test_bedrock_text_embedding_client_init_invalid_model_provider(bedrock_unit_test_env) -> None:
"""Test initialization of Amazon Bedrock Text Embedding service with invalid settings"""
with pytest.raises(
ServiceInitializationError, match="Failed to initialize the Amazon Bedrock Text Embedding Service."
):
BedrockTextEmbedding(model_provider="invalid_provider")
@patch.object(boto3, "client", return_value=Mock())
def test_prompt_execution_settings_class(mock_client, bedrock_unit_test_env) -> None:
"""Test getting prompt execution settings class"""
bedrock_completion_client = BedrockTextEmbedding()
assert bedrock_completion_client.get_prompt_execution_settings_class() == BedrockEmbeddingPromptExecutionSettings
# endregion
@pytest.mark.parametrize(
# These are fake model ids with the supported prefixes
"model_id",
[
"amazon.titan",
"cohere.command",
],
indirect=True,
)
async def test_bedrock_text_embedding(model_id, mock_bedrock_text_embedding_response) -> None:
"""Test Bedrock text embedding generation"""
with patch.object(
MockBedrockRuntimeClient, "invoke_model", return_value=mock_bedrock_text_embedding_response
) as mock_model_invoke:
# Setup
bedrock_text_embedding = BedrockTextEmbedding(
model_id=model_id,
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
)
# Act
settings = BedrockEmbeddingPromptExecutionSettings()
response = await bedrock_text_embedding.generate_embeddings(["hello", "world"], settings)
# Assert
mock_model_invoke.assert_called_with(
body=ANY,
modelId=model_id,
accept="application/json",
contentType="application/json",
)
assert mock_model_invoke.call_count == 2
assert len(response) == 2
@pytest.mark.parametrize(
# These are fake model ids with the supported prefixes
"model_id",
[
"arn:aws:bedrock:us-east-1:972143716085:application-inference-profile/123456",
],
indirect=True,
)
async def test_bedrock_text_embedding_with_application_inference_profile(
model_id,
mock_bedrock_text_embedding_response,
model_provider,
) -> None:
"""Test Bedrock text embedding generation"""
with patch.object(
MockBedrockRuntimeClient, "invoke_model", return_value=mock_bedrock_text_embedding_response
) as mock_model_invoke:
# Setup
bedrock_text_embedding = BedrockTextEmbedding(
model_id=model_id,
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
model_provider=BedrockModelProvider.AMAZON,
)
# Act
settings = BedrockEmbeddingPromptExecutionSettings()
response = await bedrock_text_embedding.generate_embeddings(["hello", "world"], settings)
# Assert
mock_model_invoke.assert_called_with(
body=ANY,
modelId=model_id,
accept="application/json",
contentType="application/json",
)
assert mock_model_invoke.call_count == 2
assert len(response) == 2
@pytest.mark.parametrize(
# These are fake model ids with the supported prefixes
"model_id",
[
"amazon.titan",
"cohere.command",
],
indirect=True,
)
async def test_bedrock_text_embedding_with_invalid_response(
model_id, mock_bedrock_text_embedding_invalid_response
) -> None:
"""Test Bedrock text embedding generation with invalid response"""
with patch.object(
MockBedrockRuntimeClient, "invoke_model", return_value=mock_bedrock_text_embedding_invalid_response
):
# Setup
bedrock_text_embedding = BedrockTextEmbedding(
model_id=model_id,
runtime_client=MockBedrockRuntimeClient(),
client=MockBedrockClient(),
)
with pytest.raises(ServiceInvalidResponseError):
await bedrock_text_embedding.generate_embeddings(["hello", "world"])
@@ -0,0 +1,104 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.connectors.ai.bedrock import BedrockChatPromptExecutionSettings, BedrockPromptExecutionSettings
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
def test_default_bedrock_prompt_execution_settings():
settings = BedrockPromptExecutionSettings()
assert settings.temperature is None
assert settings.top_p is None
assert settings.top_k is None
assert settings.max_tokens is None
assert settings.stop == []
def test_custom_bedrock_prompt_execution_settings():
settings = BedrockPromptExecutionSettings(
temperature=0.5,
top_p=0.5,
top_k=10,
max_tokens=128,
stop=["world"],
)
assert settings.temperature == 0.5
assert settings.top_p == 0.5
assert settings.top_k == 10
assert settings.max_tokens == 128
assert settings.stop == ["world"]
def test_bedrock_prompt_execution_settings_from_default_completion_config():
settings = PromptExecutionSettings(service_id="test_service")
chat_settings = BedrockChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.service_id == "test_service"
assert chat_settings.temperature is None
assert chat_settings.top_p is None
assert chat_settings.top_k is None
assert chat_settings.max_tokens is None
assert chat_settings.stop == []
def test_bedrock_prompt_execution_settings_from_openai_prompt_execution_settings():
chat_settings = BedrockChatPromptExecutionSettings(service_id="test_service", temperature=1.0)
new_settings = BedrockPromptExecutionSettings(service_id="test_2", temperature=0.0)
chat_settings.update_from_prompt_execution_settings(new_settings)
assert chat_settings.service_id == "test_2"
assert chat_settings.temperature == 0.0
def test_bedrock_prompt_execution_settings_from_custom_completion_config():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"temperature": 0.5,
"top_p": 0.5,
"top_k": 10,
"max_tokens": 128,
"stop": ["world"],
},
)
chat_settings = BedrockChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.temperature == 0.5
assert chat_settings.top_p == 0.5
assert chat_settings.top_k == 10
assert chat_settings.max_tokens == 128
assert chat_settings.stop == ["world"]
def test_bedrock_chat_prompt_execution_settings_from_custom_completion_config_with_functions():
settings = PromptExecutionSettings(
service_id="test_service",
extension_data={
"tools": [{"function": {}}],
},
)
chat_settings = BedrockChatPromptExecutionSettings.from_prompt_execution_settings(settings)
assert chat_settings.tools == [{"function": {}}]
def test_create_options():
settings = BedrockPromptExecutionSettings(
service_id="test_service",
extension_data={
"temperature": 0.5,
"top_p": 0.5,
"top_k": 10,
"max_tokens": 128,
"stop": ["world"],
},
)
options = settings.prepare_settings_dict()
assert options["temperature"] == 0.5
assert options["top_p"] == 0.5
assert options["top_k"] == 10
assert options["max_tokens"] == 128
assert options["stop"] == ["world"]