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