chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
import pytest
|
||||
|
||||
from semantic_kernel.agents.bedrock.models.bedrock_agent_event_type import BedrockAgentEventType
|
||||
from semantic_kernel.agents.bedrock.models.bedrock_agent_model import BedrockAgentModel
|
||||
from semantic_kernel.agents.bedrock.models.bedrock_agent_status import BedrockAgentStatus
|
||||
from semantic_kernel.kernel import Kernel
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def bedrock_agent_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
|
||||
"""Fixture to set environment variables for Amazon Bedrock Agent unit tests."""
|
||||
if exclude_list is None:
|
||||
exclude_list = []
|
||||
|
||||
if override_env_param_dict is None:
|
||||
override_env_param_dict = {}
|
||||
|
||||
env_vars = {
|
||||
"BEDROCK_AGENT_AGENT_RESOURCE_ROLE_ARN": "TEST_BEDROCK_AGENT_AGENT_RESOURCE_ROLE_ARN",
|
||||
"BEDROCK_AGENT_FOUNDATION_MODEL": "TEST_BEDROCK_AGENT_FOUNDATION_MODEL",
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def kernel_with_function(kernel: Kernel, decorated_native_function: Callable) -> Kernel:
|
||||
kernel.add_function("test_plugin", function=decorated_native_function)
|
||||
|
||||
return kernel
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def new_agent_name():
|
||||
return "test_agent_name"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bedrock_agent_model():
|
||||
return BedrockAgentModel(
|
||||
agent_name="test_agent_name",
|
||||
foundation_model="test_foundation_model",
|
||||
agent_status=BedrockAgentStatus.NOT_PREPARED,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bedrock_agent_model_with_id():
|
||||
return BedrockAgentModel(
|
||||
agent_id="test_agent_id",
|
||||
agent_name="test_agent_name",
|
||||
foundation_model="test_foundation_model",
|
||||
agent_status=BedrockAgentStatus.NOT_PREPARED,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bedrock_agent_model_with_id_prepared_dict():
|
||||
return {
|
||||
"agent": {
|
||||
"agentId": "test_agent_id",
|
||||
"agentName": "test_agent_name",
|
||||
"foundationModel": "test_foundation_model",
|
||||
"agentStatus": "PREPARED",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bedrock_agent_model_with_id_preparing_dict():
|
||||
return {
|
||||
"agent": {
|
||||
"agentId": "test_agent_id",
|
||||
"agentName": "test_agent_name",
|
||||
"foundationModel": "test_foundation_model",
|
||||
"agentStatus": "PREPARING",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bedrock_agent_model_with_id_not_prepared_dict():
|
||||
return {
|
||||
"agent": {
|
||||
"agentId": "test_agent_id",
|
||||
"agentName": "test_agent_name",
|
||||
"foundationModel": "test_foundation_model",
|
||||
"agentStatus": "NOT_PREPARED",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def existing_agent_not_prepared_model():
|
||||
return BedrockAgentModel(
|
||||
agent_id="test_agent_id",
|
||||
agent_name="test_agent_name",
|
||||
foundation_model="test_foundation_model",
|
||||
agent_status=BedrockAgentStatus.NOT_PREPARED,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bedrock_action_group_mode_dict():
|
||||
return {
|
||||
"agentActionGroup": {
|
||||
"actionGroupId": "test_action_group_id",
|
||||
"actionGroupName": "test_action_group_name",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def simple_response():
|
||||
return "test response"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bedrock_agent_non_streaming_empty_response():
|
||||
return {
|
||||
"completion": [],
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bedrock_agent_non_streaming_simple_response(simple_response):
|
||||
return {
|
||||
"completion": [
|
||||
{
|
||||
"chunk": {"bytes": bytes(simple_response, "utf-8")},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bedrock_agent_streaming_simple_response(simple_response):
|
||||
return {
|
||||
"completion": [
|
||||
{
|
||||
"chunk": {"bytes": bytes(chunk, "utf-8")},
|
||||
}
|
||||
for chunk in simple_response
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bedrock_agent_function_call_response():
|
||||
return {
|
||||
"completion": [
|
||||
{
|
||||
BedrockAgentEventType.RETURN_CONTROL: {
|
||||
"invocationId": "test_invocation_id",
|
||||
"invocationInputs": [
|
||||
{
|
||||
"functionInvocationInput": {
|
||||
"function": "test_function",
|
||||
"parameters": [
|
||||
{"name": "test_parameter_name", "value": "test_parameter_value"},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bedrock_agent_create_session_response():
|
||||
return {
|
||||
"sessionId": "test_session_id",
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import pytest
|
||||
|
||||
from semantic_kernel.agents.bedrock.action_group_utils import (
|
||||
BEDROCK_FUNCTION_ALLOWED_PARAMETER_TYPES,
|
||||
kernel_function_parameter_type_to_bedrock_function_parameter_type,
|
||||
kernel_function_to_bedrock_function_schema,
|
||||
parse_function_result_contents,
|
||||
parse_return_control_payload,
|
||||
)
|
||||
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
|
||||
from semantic_kernel.contents.function_result_content import FunctionResultContent
|
||||
from semantic_kernel.kernel import Kernel
|
||||
|
||||
|
||||
def test_kernel_function_to_bedrock_function_schema(kernel_with_function: Kernel):
|
||||
# Test the conversion of kernel function to bedrock function schema
|
||||
function_choice_behavior = FunctionChoiceBehavior.Auto()
|
||||
function_choice_configuration = function_choice_behavior.get_config(kernel_with_function)
|
||||
result = kernel_function_to_bedrock_function_schema(function_choice_configuration)
|
||||
assert result == {
|
||||
"functions": [
|
||||
{
|
||||
"name": "test_plugin-getLightStatus",
|
||||
"parameters": {
|
||||
"arg1": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
}
|
||||
},
|
||||
"requireConfirmation": "DISABLED",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_kernel_function_parameter_type_to_bedrock_function_parameter_type():
|
||||
# Test the conversion of kernel function parameter type to bedrock function parameter type
|
||||
schema_data = {"type": "string"}
|
||||
result = kernel_function_parameter_type_to_bedrock_function_parameter_type(schema_data)
|
||||
assert result == "string"
|
||||
|
||||
|
||||
def test_kernel_function_parameter_type_to_bedrock_function_parameter_type_invalid():
|
||||
# Test the conversion of invalid kernel function parameter type to bedrock function parameter type
|
||||
schema_data = {"type": "invalid_type"}
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="Type invalid_type is not allowed in bedrock function parameter type. "
|
||||
f"Allowed types are {BEDROCK_FUNCTION_ALLOWED_PARAMETER_TYPES}.",
|
||||
):
|
||||
kernel_function_parameter_type_to_bedrock_function_parameter_type(schema_data)
|
||||
|
||||
|
||||
def test_parse_return_control_payload():
|
||||
# Test the parsing of return control payload to function call contents
|
||||
return_control_payload = {
|
||||
"invocationId": "test_invocation_id",
|
||||
"invocationInputs": [
|
||||
{
|
||||
"functionInvocationInput": {
|
||||
"function": "test_function",
|
||||
"parameters": [
|
||||
{"name": "param1", "value": "value1"},
|
||||
{"name": "param2", "value": "value2"},
|
||||
],
|
||||
}
|
||||
}
|
||||
],
|
||||
}
|
||||
result = parse_return_control_payload(return_control_payload)
|
||||
assert len(result) == 1
|
||||
assert result[0].id == "test_invocation_id"
|
||||
assert result[0].name == "test_function"
|
||||
assert result[0].arguments == {"param1": "value1", "param2": "value2"}
|
||||
|
||||
|
||||
def test_parse_function_result_contents():
|
||||
# Test the parsing of function result contents to be returned to the agent
|
||||
function_result_contents = [
|
||||
FunctionResultContent(
|
||||
id="test_id",
|
||||
name="test_function",
|
||||
result="test_result",
|
||||
metadata={"functionInvocationInput": {"actionGroup": "test_action_group"}},
|
||||
)
|
||||
]
|
||||
result = parse_function_result_contents(function_result_contents)
|
||||
assert len(result) == 1
|
||||
assert result[0]["functionResult"]["actionGroup"] == "test_action_group"
|
||||
assert result[0]["functionResult"]["function"] == "test_function"
|
||||
assert result[0]["functionResult"]["responseBody"]["TEXT"]["body"] == "test_result"
|
||||
@@ -0,0 +1,33 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from semantic_kernel.agents.bedrock.models.bedrock_action_group_model import BedrockActionGroupModel
|
||||
|
||||
|
||||
def test_bedrock_action_group_model_valid():
|
||||
"""Test case to verify the BedrockActionGroupModel with valid data."""
|
||||
model = BedrockActionGroupModel(actionGroupId="test_id", actionGroupName="test_name")
|
||||
assert model.action_group_id == "test_id"
|
||||
assert model.action_group_name == "test_name"
|
||||
|
||||
|
||||
def test_bedrock_action_group_model_missing_action_group_id():
|
||||
"""Test case to verify error handling when actionGroupId is missing."""
|
||||
with pytest.raises(ValidationError):
|
||||
BedrockActionGroupModel(actionGroupName="test_name")
|
||||
|
||||
|
||||
def test_bedrock_action_group_model_missing_action_group_name():
|
||||
"""Test case to verify error handling when actionGroupName is missing."""
|
||||
with pytest.raises(ValidationError):
|
||||
BedrockActionGroupModel(actionGroupId="test_id")
|
||||
|
||||
|
||||
def test_bedrock_action_group_model_extra_field():
|
||||
"""Test case to verify the BedrockActionGroupModel with an extra field."""
|
||||
model = BedrockActionGroupModel(actionGroupId="test_id", actionGroupName="test_name", extraField="extra_value")
|
||||
assert model.action_group_id == "test_id"
|
||||
assert model.action_group_name == "test_name"
|
||||
assert model.extraField == "extra_value"
|
||||
@@ -0,0 +1,751 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from unittest.mock import AsyncMock, Mock, PropertyMock, patch
|
||||
|
||||
import boto3
|
||||
import pytest
|
||||
|
||||
from semantic_kernel.agents.bedrock.action_group_utils import (
|
||||
kernel_function_to_bedrock_function_schema,
|
||||
parse_function_result_contents,
|
||||
)
|
||||
from semantic_kernel.agents.bedrock.bedrock_agent import BedrockAgent, BedrockAgentThread
|
||||
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
|
||||
from semantic_kernel.contents.function_result_content import FunctionResultContent
|
||||
from semantic_kernel.exceptions.agent_exceptions import AgentInitializationException, AgentInvokeException
|
||||
from semantic_kernel.functions.kernel_arguments import KernelArguments
|
||||
from semantic_kernel.kernel import Kernel
|
||||
|
||||
# region Agent Initialization Tests
|
||||
|
||||
|
||||
# Test case to verify BedrockAgent initialization
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
async def test_bedrock_agent_initialization(client, bedrock_agent_model_with_id):
|
||||
agent = BedrockAgent(bedrock_agent_model_with_id)
|
||||
|
||||
assert agent.name == bedrock_agent_model_with_id.agent_name
|
||||
assert agent.agent_model.agent_name == bedrock_agent_model_with_id.agent_name
|
||||
assert agent.agent_model.agent_id == bedrock_agent_model_with_id.agent_id
|
||||
assert agent.agent_model.foundation_model == bedrock_agent_model_with_id.foundation_model
|
||||
|
||||
|
||||
# Test case to verify error handling during BedrockAgent initialization with non-auto function choice
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
async def test_bedrock_agent_initialization_error_with_non_auto_function_choice(client, bedrock_agent_model_with_id):
|
||||
with pytest.raises(ValueError, match="Only FunctionChoiceType.AUTO is supported."):
|
||||
BedrockAgent(
|
||||
bedrock_agent_model_with_id,
|
||||
function_choice_behavior=FunctionChoiceBehavior.NoneInvoke(),
|
||||
)
|
||||
|
||||
|
||||
# Test case to verify the creation of BedrockAgent
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
@pytest.mark.parametrize(
|
||||
"kernel, function_choice_behavior, arguments",
|
||||
[
|
||||
(None, None, None),
|
||||
(Kernel(), None, None),
|
||||
(Kernel(), FunctionChoiceBehavior.Auto(), None),
|
||||
(Kernel(), FunctionChoiceBehavior.Auto(), KernelArguments()),
|
||||
],
|
||||
)
|
||||
async def test_bedrock_agent_create_and_prepare_agent(
|
||||
client,
|
||||
bedrock_agent_model_with_id_not_prepared_dict,
|
||||
bedrock_agent_unit_test_env,
|
||||
kernel,
|
||||
function_choice_behavior,
|
||||
arguments,
|
||||
):
|
||||
with (
|
||||
patch.object(client, "create_agent") as mock_create_agent,
|
||||
patch.object(BedrockAgent, "_wait_for_agent_status", new_callable=AsyncMock),
|
||||
patch.object(BedrockAgent, "prepare_agent_and_wait_until_prepared", new_callable=AsyncMock),
|
||||
):
|
||||
mock_create_agent.return_value = bedrock_agent_model_with_id_not_prepared_dict
|
||||
|
||||
agent = await BedrockAgent.create_and_prepare_agent(
|
||||
name=bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentName"],
|
||||
instructions="test_instructions",
|
||||
bedrock_client=client,
|
||||
env_file_path="fake_path",
|
||||
kernel=kernel,
|
||||
function_choice_behavior=function_choice_behavior,
|
||||
arguments=arguments,
|
||||
)
|
||||
|
||||
mock_create_agent.assert_called_once_with(
|
||||
agentName=bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentName"],
|
||||
foundationModel=bedrock_agent_unit_test_env["BEDROCK_AGENT_FOUNDATION_MODEL"],
|
||||
agentResourceRoleArn=bedrock_agent_unit_test_env["BEDROCK_AGENT_AGENT_RESOURCE_ROLE_ARN"],
|
||||
instruction="test_instructions",
|
||||
)
|
||||
assert agent.agent_model.agent_id == bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentId"]
|
||||
assert agent.id == bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentId"]
|
||||
assert agent.agent_model.agent_name == bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentName"]
|
||||
assert agent.name == bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentName"]
|
||||
assert (
|
||||
agent.agent_model.foundation_model
|
||||
== bedrock_agent_model_with_id_not_prepared_dict["agent"]["foundationModel"]
|
||||
)
|
||||
assert agent.kernel is not None
|
||||
assert agent.function_choice_behavior is not None
|
||||
if arguments:
|
||||
assert agent.arguments is not None
|
||||
|
||||
|
||||
# Test case to verify the creation of BedrockAgent
|
||||
@pytest.mark.parametrize(
|
||||
"exclude_list",
|
||||
[
|
||||
["BEDROCK_AGENT_AGENT_RESOURCE_ROLE_ARN"],
|
||||
["BEDROCK_AGENT_FOUNDATION_MODEL"],
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
async def test_bedrock_agent_create_and_prepare_agent_settings_validation_error(
|
||||
client,
|
||||
bedrock_agent_model_with_id_not_prepared_dict,
|
||||
bedrock_agent_unit_test_env,
|
||||
):
|
||||
with pytest.raises(AgentInitializationException):
|
||||
await BedrockAgent.create_and_prepare_agent(
|
||||
name=bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentName"],
|
||||
instructions="test_instructions",
|
||||
env_file_path="fake_path",
|
||||
)
|
||||
|
||||
|
||||
# Test case to verify the creation of BedrockAgent
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
async def test_bedrock_agent_create_and_prepare_agent_service_exception(
|
||||
client,
|
||||
bedrock_agent_model_with_id_not_prepared_dict,
|
||||
bedrock_agent_unit_test_env,
|
||||
):
|
||||
with (
|
||||
patch.object(client, "create_agent") as mock_create_agent,
|
||||
patch.object(BedrockAgent, "prepare_agent_and_wait_until_prepared", new_callable=AsyncMock),
|
||||
):
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
mock_create_agent.side_effect = ClientError({}, "create_agent")
|
||||
|
||||
with pytest.raises(AgentInitializationException):
|
||||
await BedrockAgent.create_and_prepare_agent(
|
||||
name=bedrock_agent_model_with_id_not_prepared_dict["agent"]["agentName"],
|
||||
instructions="test_instructions",
|
||||
bedrock_client=client,
|
||||
env_file_path="fake_path",
|
||||
)
|
||||
|
||||
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
async def test_bedrock_agent_prepare_agent_and_wait_until_prepared(
|
||||
client,
|
||||
bedrock_agent_unit_test_env,
|
||||
bedrock_agent_model_with_id,
|
||||
bedrock_agent_model_with_id_preparing_dict,
|
||||
bedrock_agent_model_with_id_prepared_dict,
|
||||
):
|
||||
agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)
|
||||
|
||||
with (
|
||||
patch.object(client, "get_agent") as mock_get_agent,
|
||||
patch.object(client, "prepare_agent") as mock_prepare_agent,
|
||||
):
|
||||
mock_get_agent.side_effect = [
|
||||
bedrock_agent_model_with_id_preparing_dict,
|
||||
bedrock_agent_model_with_id_prepared_dict,
|
||||
]
|
||||
|
||||
await agent.prepare_agent_and_wait_until_prepared()
|
||||
|
||||
mock_prepare_agent.assert_called_once_with(agentId=bedrock_agent_model_with_id.agent_id)
|
||||
assert mock_get_agent.call_count == 2
|
||||
assert agent.agent_model.agent_status == "PREPARED"
|
||||
|
||||
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
async def test_bedrock_agent_prepare_agent_and_wait_until_prepared_fail(
|
||||
client,
|
||||
bedrock_agent_unit_test_env,
|
||||
bedrock_agent_model_with_id,
|
||||
bedrock_agent_model_with_id_preparing_dict,
|
||||
):
|
||||
agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)
|
||||
|
||||
with (
|
||||
patch.object(client, "get_agent") as mock_get_agent,
|
||||
patch.object(client, "prepare_agent"),
|
||||
):
|
||||
mock_get_agent.side_effect = [
|
||||
bedrock_agent_model_with_id_preparing_dict,
|
||||
bedrock_agent_model_with_id_preparing_dict,
|
||||
bedrock_agent_model_with_id_preparing_dict,
|
||||
bedrock_agent_model_with_id_preparing_dict,
|
||||
bedrock_agent_model_with_id_preparing_dict,
|
||||
bedrock_agent_model_with_id_preparing_dict,
|
||||
]
|
||||
|
||||
with pytest.raises(TimeoutError):
|
||||
await agent.prepare_agent_and_wait_until_prepared()
|
||||
|
||||
|
||||
# Test case to verify the creation of a code interpreter action group
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
async def test_create_code_interpreter_action_group(
|
||||
client,
|
||||
bedrock_agent_unit_test_env,
|
||||
bedrock_agent_model_with_id,
|
||||
bedrock_action_group_mode_dict,
|
||||
):
|
||||
agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)
|
||||
|
||||
with (
|
||||
patch.object(client, "create_agent_action_group") as mock_create_action_group,
|
||||
patch.object(
|
||||
BedrockAgent, "prepare_agent_and_wait_until_prepared"
|
||||
) as mock_prepare_agent_and_wait_until_prepared,
|
||||
):
|
||||
mock_create_action_group.return_value = bedrock_action_group_mode_dict
|
||||
action_group_model = await agent.create_code_interpreter_action_group()
|
||||
|
||||
mock_create_action_group.assert_called_once_with(
|
||||
agentId=agent.agent_model.agent_id,
|
||||
agentVersion=agent.agent_model.agent_version or "DRAFT",
|
||||
actionGroupName=f"{agent.agent_model.agent_name}_code_interpreter",
|
||||
actionGroupState="ENABLED",
|
||||
parentActionGroupSignature="AMAZON.CodeInterpreter",
|
||||
)
|
||||
assert action_group_model.action_group_id == bedrock_action_group_mode_dict["agentActionGroup"]["actionGroupId"]
|
||||
mock_prepare_agent_and_wait_until_prepared.assert_called_once()
|
||||
|
||||
|
||||
# Test case to verify the creation of BedrockAgent with plugins
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
async def test_bedrock_agent_create_with_plugin_via_constructor(
|
||||
client,
|
||||
bedrock_agent_unit_test_env,
|
||||
bedrock_agent_model_with_id,
|
||||
custom_plugin_class,
|
||||
):
|
||||
agent = BedrockAgent(
|
||||
bedrock_agent_model_with_id,
|
||||
plugins=[custom_plugin_class()],
|
||||
bedrock_client=client,
|
||||
)
|
||||
|
||||
assert agent.kernel.plugins is not None
|
||||
assert len(agent.kernel.plugins) == 1
|
||||
|
||||
|
||||
# Test case to verify the creation of a user input action group
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
async def test_create_user_input_action_group(
|
||||
client,
|
||||
bedrock_agent_unit_test_env,
|
||||
bedrock_agent_model_with_id,
|
||||
bedrock_action_group_mode_dict,
|
||||
):
|
||||
agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)
|
||||
|
||||
with (
|
||||
patch.object(agent.bedrock_client, "create_agent_action_group") as mock_create_action_group,
|
||||
patch.object(
|
||||
BedrockAgent, "prepare_agent_and_wait_until_prepared"
|
||||
) as mock_prepare_agent_and_wait_until_prepared,
|
||||
):
|
||||
mock_create_action_group.return_value = bedrock_action_group_mode_dict
|
||||
action_group_model = await agent.create_user_input_action_group()
|
||||
|
||||
mock_create_action_group.assert_called_once_with(
|
||||
agentId=agent.agent_model.agent_id,
|
||||
agentVersion=agent.agent_model.agent_version or "DRAFT",
|
||||
actionGroupName=f"{agent.agent_model.agent_name}_user_input",
|
||||
actionGroupState="ENABLED",
|
||||
parentActionGroupSignature="AMAZON.UserInput",
|
||||
)
|
||||
assert action_group_model.action_group_id == bedrock_action_group_mode_dict["agentActionGroup"]["actionGroupId"]
|
||||
mock_prepare_agent_and_wait_until_prepared.assert_called_once()
|
||||
|
||||
|
||||
# Test case to verify the creation of a kernel function action group
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
async def test_create_kernel_function_action_group(
|
||||
client,
|
||||
kernel_with_function,
|
||||
bedrock_agent_unit_test_env,
|
||||
bedrock_agent_model_with_id,
|
||||
bedrock_action_group_mode_dict,
|
||||
):
|
||||
agent = BedrockAgent(bedrock_agent_model_with_id, kernel=kernel_with_function, bedrock_client=client)
|
||||
|
||||
with (
|
||||
patch.object(agent.bedrock_client, "create_agent_action_group") as mock_create_action_group,
|
||||
patch.object(
|
||||
BedrockAgent, "prepare_agent_and_wait_until_prepared"
|
||||
) as mock_prepare_agent_and_wait_until_prepared,
|
||||
):
|
||||
mock_create_action_group.return_value = bedrock_action_group_mode_dict
|
||||
|
||||
action_group_model = await agent.create_kernel_function_action_group()
|
||||
|
||||
mock_create_action_group.assert_called_once_with(
|
||||
agentId=agent.agent_model.agent_id,
|
||||
agentVersion=agent.agent_model.agent_version or "DRAFT",
|
||||
actionGroupName=f"{agent.agent_model.agent_name}_kernel_function",
|
||||
actionGroupState="ENABLED",
|
||||
actionGroupExecutor={"customControl": "RETURN_CONTROL"},
|
||||
functionSchema=kernel_function_to_bedrock_function_schema(
|
||||
agent.function_choice_behavior.get_config(kernel_with_function)
|
||||
),
|
||||
)
|
||||
assert action_group_model.action_group_id == bedrock_action_group_mode_dict["agentActionGroup"]["actionGroupId"]
|
||||
mock_prepare_agent_and_wait_until_prepared.assert_called_once()
|
||||
|
||||
|
||||
# Test case to verify the association of an agent with a knowledge base
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
async def test_associate_agent_knowledge_base(
|
||||
client,
|
||||
bedrock_agent_unit_test_env,
|
||||
bedrock_agent_model_with_id,
|
||||
):
|
||||
agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)
|
||||
|
||||
with (
|
||||
patch.object(agent.bedrock_client, "associate_agent_knowledge_base") as mock_associate_knowledge_base,
|
||||
patch.object(
|
||||
BedrockAgent, "prepare_agent_and_wait_until_prepared"
|
||||
) as mock_prepare_agent_and_wait_until_prepared,
|
||||
):
|
||||
await agent.associate_agent_knowledge_base("test_knowledge_base_id")
|
||||
|
||||
mock_associate_knowledge_base.assert_called_once_with(
|
||||
agentId=agent.agent_model.agent_id,
|
||||
agentVersion=agent.agent_model.agent_version,
|
||||
knowledgeBaseId="test_knowledge_base_id",
|
||||
)
|
||||
mock_prepare_agent_and_wait_until_prepared.assert_called_once()
|
||||
|
||||
|
||||
# Test case to verify the disassociation of an agent with a knowledge base
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
async def test_disassociate_agent_knowledge_base(
|
||||
client,
|
||||
bedrock_agent_unit_test_env,
|
||||
bedrock_agent_model_with_id,
|
||||
):
|
||||
agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)
|
||||
|
||||
with (
|
||||
patch.object(agent.bedrock_client, "disassociate_agent_knowledge_base") as mock_disassociate_knowledge_base,
|
||||
patch.object(
|
||||
BedrockAgent, "prepare_agent_and_wait_until_prepared"
|
||||
) as mock_prepare_agent_and_wait_until_prepared,
|
||||
):
|
||||
await agent.disassociate_agent_knowledge_base("test_knowledge_base_id")
|
||||
mock_disassociate_knowledge_base.assert_called_once_with(
|
||||
agentId=agent.agent_model.agent_id,
|
||||
agentVersion=agent.agent_model.agent_version,
|
||||
knowledgeBaseId="test_knowledge_base_id",
|
||||
)
|
||||
mock_prepare_agent_and_wait_until_prepared.assert_called_once()
|
||||
|
||||
|
||||
# Test case to verify listing associated knowledge bases with an agent
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
async def test_list_associated_agent_knowledge_bases(
|
||||
client,
|
||||
bedrock_agent_unit_test_env,
|
||||
bedrock_agent_model_with_id,
|
||||
):
|
||||
agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)
|
||||
|
||||
with patch.object(agent.bedrock_client, "list_agent_knowledge_bases") as mock_list_knowledge_bases:
|
||||
await agent.list_associated_agent_knowledge_bases()
|
||||
|
||||
mock_list_knowledge_bases.assert_called_once_with(
|
||||
agentId=agent.agent_model.agent_id,
|
||||
agentVersion=agent.agent_model.agent_version,
|
||||
)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Agent Deletion Tests
|
||||
|
||||
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
async def test_delete_agent(
|
||||
client,
|
||||
bedrock_agent_unit_test_env,
|
||||
bedrock_agent_model_with_id,
|
||||
):
|
||||
agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)
|
||||
|
||||
agent_id = bedrock_agent_model_with_id.agent_id
|
||||
with patch.object(agent.bedrock_client, "delete_agent") as mock_delete_agent:
|
||||
await agent.delete_agent()
|
||||
|
||||
mock_delete_agent.assert_called_once_with(agentId=agent_id)
|
||||
assert agent.agent_model.agent_id is None
|
||||
|
||||
|
||||
# Test case to verify error handling when deleting an agent that does not exist
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
async def test_delete_agent_twice_error(
|
||||
client,
|
||||
bedrock_agent_unit_test_env,
|
||||
bedrock_agent_model_with_id,
|
||||
):
|
||||
agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)
|
||||
|
||||
with patch.object(agent.bedrock_client, "delete_agent"):
|
||||
await agent.delete_agent()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await agent.delete_agent()
|
||||
|
||||
|
||||
# Test case to verify error handling when there is a client error during agent deletion
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
async def test_delete_agent_client_error(
|
||||
client,
|
||||
bedrock_agent_unit_test_env,
|
||||
bedrock_agent_model_with_id,
|
||||
):
|
||||
agent = BedrockAgent(bedrock_agent_model_with_id, bedrock_client=client)
|
||||
|
||||
with patch.object(agent.bedrock_client, "delete_agent") as mock_delete_agent:
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
mock_delete_agent.side_effect = ClientError({"Error": {"Code": "500"}}, "delete_agent")
|
||||
|
||||
with pytest.raises(ClientError):
|
||||
await agent.delete_agent()
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Agent Invoke Tests
|
||||
|
||||
|
||||
# Test case to verify the `get_response` method of BedrockAgent
|
||||
@pytest.mark.parametrize(
|
||||
"thread",
|
||||
[
|
||||
None,
|
||||
BedrockAgentThread(None, session_id="test_session_id"),
|
||||
],
|
||||
)
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
async def test_bedrock_agent_get_response(
|
||||
client,
|
||||
thread,
|
||||
bedrock_agent_unit_test_env,
|
||||
bedrock_agent_model_with_id,
|
||||
bedrock_agent_non_streaming_simple_response,
|
||||
simple_response,
|
||||
):
|
||||
with (
|
||||
patch.object(BedrockAgent, "_invoke_agent", new_callable=AsyncMock) as mock_invoke_agent,
|
||||
patch.object(BedrockAgentThread, "create", new_callable=AsyncMock) as mock_start,
|
||||
patch(
|
||||
"semantic_kernel.agents.bedrock.bedrock_agent.BedrockAgentThread.id",
|
||||
new_callable=PropertyMock,
|
||||
) as mock_id,
|
||||
):
|
||||
agent = BedrockAgent(bedrock_agent_model_with_id)
|
||||
mock_id.return_value = "mock-thread-id"
|
||||
|
||||
mock_invoke_agent.return_value = bedrock_agent_non_streaming_simple_response
|
||||
mock_start.return_value = "test_session_id"
|
||||
|
||||
response = await agent.get_response(messages="test_input_text", thread=thread)
|
||||
assert response.message.content == simple_response
|
||||
|
||||
mock_invoke_agent.assert_called_once()
|
||||
|
||||
|
||||
# Test case to verify the `get_response` method of BedrockAgent
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
async def test_bedrock_agent_get_response_exception(
|
||||
client,
|
||||
bedrock_agent_unit_test_env,
|
||||
bedrock_agent_model_with_id,
|
||||
bedrock_agent_non_streaming_empty_response,
|
||||
):
|
||||
with (
|
||||
patch.object(BedrockAgent, "_invoke_agent", new_callable=AsyncMock) as mock_invoke_agent,
|
||||
patch.object(BedrockAgentThread, "create", new_callable=AsyncMock) as mock_start,
|
||||
patch(
|
||||
"semantic_kernel.agents.bedrock.bedrock_agent.BedrockAgentThread.id",
|
||||
new_callable=PropertyMock,
|
||||
) as mock_id,
|
||||
):
|
||||
agent = BedrockAgent(bedrock_agent_model_with_id)
|
||||
mock_id.return_value = "mock-thread-id"
|
||||
|
||||
mock_invoke_agent.return_value = bedrock_agent_non_streaming_empty_response
|
||||
mock_start.return_value = "test_session_id"
|
||||
|
||||
with pytest.raises(AgentInvokeException):
|
||||
await agent.get_response(messages="test_input_text")
|
||||
|
||||
|
||||
# Test case to verify the invocation of BedrockAgent
|
||||
@pytest.mark.parametrize(
|
||||
"thread",
|
||||
[
|
||||
None,
|
||||
BedrockAgentThread(None, session_id="test_session_id"),
|
||||
],
|
||||
)
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
async def test_bedrock_agent_invoke(
|
||||
client,
|
||||
thread,
|
||||
bedrock_agent_unit_test_env,
|
||||
bedrock_agent_model_with_id,
|
||||
bedrock_agent_non_streaming_simple_response,
|
||||
simple_response,
|
||||
):
|
||||
with (
|
||||
patch.object(BedrockAgent, "_invoke_agent", new_callable=AsyncMock) as mock_invoke_agent,
|
||||
patch.object(BedrockAgentThread, "create", new_callable=AsyncMock) as mock_start,
|
||||
patch.object(BedrockAgentThread, "id", "test_session_id"),
|
||||
):
|
||||
agent = BedrockAgent(bedrock_agent_model_with_id)
|
||||
|
||||
mock_invoke_agent.return_value = bedrock_agent_non_streaming_simple_response
|
||||
mock_start.return_value = "test_session_id"
|
||||
|
||||
async for response in agent.invoke(messages="test_input_text", thread=thread):
|
||||
assert response.message.content == simple_response
|
||||
|
||||
mock_invoke_agent.assert_called_once_with(
|
||||
"test_session_id",
|
||||
"test_input_text",
|
||||
None,
|
||||
streamingConfigurations={"streamFinalResponse": False},
|
||||
sessionState={},
|
||||
)
|
||||
|
||||
|
||||
# Test case to verify the streaming invocation of BedrockAgent
|
||||
@pytest.mark.parametrize(
|
||||
"thread",
|
||||
[
|
||||
None,
|
||||
BedrockAgentThread(None, session_id="test_session_id"),
|
||||
],
|
||||
)
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
async def test_bedrock_agent_invoke_stream(
|
||||
client,
|
||||
thread,
|
||||
bedrock_agent_unit_test_env,
|
||||
bedrock_agent_model_with_id,
|
||||
bedrock_agent_streaming_simple_response,
|
||||
simple_response,
|
||||
):
|
||||
with (
|
||||
patch.object(BedrockAgent, "_invoke_agent", new_callable=AsyncMock) as mock_invoke_agent,
|
||||
patch.object(BedrockAgentThread, "create", new_callable=AsyncMock) as mock_start,
|
||||
patch.object(BedrockAgentThread, "id", "test_session_id"),
|
||||
):
|
||||
agent = BedrockAgent(bedrock_agent_model_with_id)
|
||||
|
||||
mock_invoke_agent.return_value = bedrock_agent_streaming_simple_response
|
||||
mock_start.return_value = "test_session_id"
|
||||
|
||||
full_message = ""
|
||||
async for response in agent.invoke_stream(messages="test_input_text", thread=thread):
|
||||
full_message += response.message.content
|
||||
|
||||
assert full_message == simple_response
|
||||
mock_invoke_agent.assert_called_once_with(
|
||||
"test_session_id",
|
||||
"test_input_text",
|
||||
None,
|
||||
streamingConfigurations={"streamFinalResponse": True},
|
||||
sessionState={},
|
||||
)
|
||||
|
||||
|
||||
# Test case to verify the invocation of BedrockAgent with function call
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
async def test_bedrock_agent_invoke_with_function_call(
|
||||
client,
|
||||
bedrock_agent_unit_test_env,
|
||||
bedrock_agent_model_with_id,
|
||||
bedrock_agent_function_call_response,
|
||||
bedrock_agent_non_streaming_simple_response,
|
||||
):
|
||||
with (
|
||||
patch.object(BedrockAgent, "_invoke_agent", new_callable=AsyncMock) as mock_invoke_agent,
|
||||
patch.object(BedrockAgent, "_handle_function_call_contents") as mock_handle_function_call_contents,
|
||||
patch.object(BedrockAgentThread, "create", new_callable=AsyncMock) as mock_start,
|
||||
patch.object(BedrockAgentThread, "id", "test_session_id"),
|
||||
):
|
||||
agent = BedrockAgent(bedrock_agent_model_with_id)
|
||||
|
||||
function_result_contents = [
|
||||
FunctionResultContent(
|
||||
id="test_id",
|
||||
name="test_function",
|
||||
result="test_result",
|
||||
metadata={"functionInvocationInput": {"actionGroup": "test_action_group"}},
|
||||
)
|
||||
]
|
||||
mock_handle_function_call_contents.return_value = function_result_contents
|
||||
agent.function_choice_behavior.maximum_auto_invoke_attempts = 2
|
||||
|
||||
mock_invoke_agent.side_effect = [
|
||||
bedrock_agent_function_call_response,
|
||||
bedrock_agent_non_streaming_simple_response,
|
||||
]
|
||||
mock_start.return_value = "test_session_id"
|
||||
async for _ in agent.invoke(messages="test_input_text"):
|
||||
mock_invoke_agent.assert_called_with(
|
||||
"test_session_id",
|
||||
"test_input_text",
|
||||
None,
|
||||
streamingConfigurations={"streamFinalResponse": False},
|
||||
sessionState={
|
||||
"invocationId": "test_invocation_id",
|
||||
"returnControlInvocationResults": parse_function_result_contents(function_result_contents),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# Test case to verify the streaming invocation of BedrockAgent with function call
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
async def test_bedrock_agent_invoke_stream_with_function_call(
|
||||
client,
|
||||
bedrock_agent_unit_test_env,
|
||||
bedrock_agent_model_with_id,
|
||||
bedrock_agent_function_call_response,
|
||||
bedrock_agent_streaming_simple_response,
|
||||
):
|
||||
with (
|
||||
patch.object(BedrockAgent, "_invoke_agent", new_callable=AsyncMock) as mock_invoke_agent,
|
||||
patch.object(BedrockAgent, "_handle_function_call_contents") as mock_handle_function_call_contents,
|
||||
patch.object(BedrockAgentThread, "create", new_callable=AsyncMock) as mock_start,
|
||||
patch.object(BedrockAgentThread, "id", "test_session_id"),
|
||||
):
|
||||
agent = BedrockAgent(bedrock_agent_model_with_id)
|
||||
|
||||
function_result_contents = [
|
||||
FunctionResultContent(
|
||||
id="test_id",
|
||||
name="test_function",
|
||||
result="test_result",
|
||||
metadata={"functionInvocationInput": {"actionGroup": "test_action_group"}},
|
||||
)
|
||||
]
|
||||
mock_handle_function_call_contents.return_value = function_result_contents
|
||||
agent.function_choice_behavior.maximum_auto_invoke_attempts = 2
|
||||
|
||||
mock_invoke_agent.side_effect = [
|
||||
bedrock_agent_function_call_response,
|
||||
bedrock_agent_streaming_simple_response,
|
||||
]
|
||||
mock_start.return_value = "test_session_id"
|
||||
async for _ in agent.invoke_stream(messages="test_input_text"):
|
||||
mock_invoke_agent.assert_called_with(
|
||||
"test_session_id",
|
||||
"test_input_text",
|
||||
None,
|
||||
streamingConfigurations={"streamFinalResponse": True},
|
||||
sessionState={
|
||||
"invocationId": "test_invocation_id",
|
||||
"returnControlInvocationResults": parse_function_result_contents(function_result_contents),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region Filename Sanitization Tests
|
||||
|
||||
|
||||
def test_sanitize_filename_simple():
|
||||
"""Test _sanitize_filename with a simple filename."""
|
||||
assert BedrockAgent._sanitize_filename("file.txt") == "file.txt"
|
||||
|
||||
|
||||
def test_sanitize_filename_with_spaces():
|
||||
"""Test _sanitize_filename with spaces in filename."""
|
||||
assert BedrockAgent._sanitize_filename("my file.txt") == "my file.txt"
|
||||
|
||||
|
||||
def test_sanitize_filename_directory_traversal_unix():
|
||||
"""Test _sanitize_filename strips Unix-style directory traversal."""
|
||||
assert BedrockAgent._sanitize_filename("../../../etc/passwd") == "passwd"
|
||||
assert BedrockAgent._sanitize_filename("../../file.txt") == "file.txt"
|
||||
assert BedrockAgent._sanitize_filename("/etc/passwd") == "passwd"
|
||||
|
||||
|
||||
def test_sanitize_filename_directory_traversal_windows():
|
||||
"""Test _sanitize_filename strips Windows-style directory traversal."""
|
||||
assert BedrockAgent._sanitize_filename("..\\..\\..\\Windows\\System32\\config") == "config"
|
||||
assert BedrockAgent._sanitize_filename("C:\\Users\\file.txt") == "file.txt"
|
||||
assert BedrockAgent._sanitize_filename("\\\\server\\share\\file.txt") == "file.txt"
|
||||
|
||||
|
||||
def test_sanitize_filename_mixed_separators():
|
||||
"""Test _sanitize_filename with mixed path separators."""
|
||||
assert BedrockAgent._sanitize_filename("../path\\to/file.txt") == "file.txt"
|
||||
assert BedrockAgent._sanitize_filename("..\\path/to\\file.txt") == "file.txt"
|
||||
|
||||
|
||||
def test_sanitize_filename_null_byte():
|
||||
"""Test _sanitize_filename removes null bytes."""
|
||||
assert BedrockAgent._sanitize_filename("file\x00.txt") == "file.txt"
|
||||
assert BedrockAgent._sanitize_filename("file.txt\x00.exe") == "file.txt.exe"
|
||||
|
||||
|
||||
def test_sanitize_filename_empty():
|
||||
"""Test _sanitize_filename returns empty string for empty result."""
|
||||
assert BedrockAgent._sanitize_filename("") == ""
|
||||
assert BedrockAgent._sanitize_filename("../") == ""
|
||||
assert BedrockAgent._sanitize_filename("..\\") == ""
|
||||
|
||||
|
||||
def test_sanitize_filename_only_dots():
|
||||
"""Test _sanitize_filename handles edge cases with dots."""
|
||||
# Note: os.path.basename("..") returns ".." which is kept as-is
|
||||
# Only "../" or "..\" patterns get stripped to empty string
|
||||
assert BedrockAgent._sanitize_filename(".") == "."
|
||||
|
||||
|
||||
def test_sanitize_filename_logs_warning(caplog):
|
||||
"""Test _sanitize_filename logs warning when filename is sanitized."""
|
||||
import logging
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
result = BedrockAgent._sanitize_filename("../malicious/file.txt")
|
||||
assert result == "file.txt"
|
||||
assert "potentially malicious path components" in caplog.text
|
||||
assert "../malicious/file.txt" in caplog.text
|
||||
assert "file.txt" in caplog.text
|
||||
|
||||
|
||||
def test_sanitize_filename_no_warning_for_clean_filename(caplog):
|
||||
"""Test _sanitize_filename does not log warning for clean filenames."""
|
||||
import logging
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
result = BedrockAgent._sanitize_filename("clean_file.txt")
|
||||
assert result == "clean_file.txt"
|
||||
assert "potentially malicious" not in caplog.text
|
||||
|
||||
|
||||
# endregion
|
||||
@@ -0,0 +1,331 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from collections.abc import AsyncIterable
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
import boto3
|
||||
import pytest
|
||||
|
||||
from semantic_kernel.agents.agent import Agent, AgentResponseItem, AgentThread
|
||||
from semantic_kernel.agents.bedrock.models.bedrock_agent_model import BedrockAgentModel
|
||||
from semantic_kernel.agents.channels.bedrock_agent_channel import BedrockAgentChannel
|
||||
from semantic_kernel.contents.chat_message_content import ChatMessageContent
|
||||
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
|
||||
from semantic_kernel.contents.utils.author_role import AuthorRole
|
||||
from semantic_kernel.exceptions.agent_exceptions import AgentChatException
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@patch.object(boto3, "client", return_value=Mock())
|
||||
def mock_channel(client):
|
||||
from semantic_kernel.agents.bedrock.bedrock_agent import BedrockAgentThread
|
||||
|
||||
BedrockAgentChannel.model_rebuild()
|
||||
thread = BedrockAgentThread(client, session_id="test_session_id")
|
||||
|
||||
return BedrockAgentChannel(thread=thread)
|
||||
|
||||
|
||||
class ConcreteAgent(Agent):
|
||||
async def get_response(self, *args, **kwargs) -> ChatMessageContent:
|
||||
raise NotImplementedError
|
||||
|
||||
def invoke(self, *args, **kwargs) -> AsyncIterable[ChatMessageContent]:
|
||||
raise NotImplementedError
|
||||
|
||||
def invoke_stream(self, *args, **kwargs) -> AsyncIterable[StreamingChatMessageContent]:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def chat_history() -> list[ChatMessageContent]:
|
||||
return [
|
||||
ChatMessageContent(role="user", content="Hello, Bedrock!"),
|
||||
ChatMessageContent(role="assistant", content="Hello, User!"),
|
||||
ChatMessageContent(role="user", content="How are you?"),
|
||||
ChatMessageContent(role="assistant", content="I'm good, thank you!"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def chat_history_not_alternate_role() -> list[ChatMessageContent]:
|
||||
return [
|
||||
ChatMessageContent(role="user", content="Hello, Bedrock!"),
|
||||
ChatMessageContent(role="user", content="Hello, User!"),
|
||||
ChatMessageContent(role="assistant", content="How are you?"),
|
||||
ChatMessageContent(role="assistant", content="I'm good, thank you!"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_agent():
|
||||
"""
|
||||
Fixture that creates a mock BedrockAgent.
|
||||
"""
|
||||
from semantic_kernel.agents.bedrock.bedrock_agent import BedrockAgent
|
||||
|
||||
# Create mocks
|
||||
mock_agent = MagicMock(spec=BedrockAgent)
|
||||
# Set the name and agent_model properties
|
||||
mock_agent.name = "MockBedrockAgent"
|
||||
mock_agent.agent_model = MagicMock(spec=BedrockAgentModel)
|
||||
mock_agent.agent_model.foundation_model = "mock-foundation-model"
|
||||
|
||||
return mock_agent
|
||||
|
||||
|
||||
async def test_receive_message(mock_channel, chat_history):
|
||||
# Test to verify the receive_message functionality
|
||||
await mock_channel.receive(chat_history)
|
||||
assert len(mock_channel) == len(chat_history)
|
||||
|
||||
|
||||
async def test_channel_receive_message_with_no_message(mock_channel):
|
||||
# Test to verify receive_message when no message is received
|
||||
await mock_channel.receive([])
|
||||
assert len(mock_channel) == 0
|
||||
|
||||
|
||||
async def test_chat_history_alternation(mock_channel, chat_history_not_alternate_role):
|
||||
# Test to verify chat history alternates between user and assistant messages
|
||||
await mock_channel.receive(chat_history_not_alternate_role)
|
||||
assert all(
|
||||
mock_channel.messages[i].role != mock_channel.messages[i + 1].role
|
||||
for i in range(len(chat_history_not_alternate_role) - 1)
|
||||
)
|
||||
assert mock_channel.messages[1].content == mock_channel.MESSAGE_PLACEHOLDER
|
||||
assert mock_channel.messages[4].content == mock_channel.MESSAGE_PLACEHOLDER
|
||||
|
||||
|
||||
async def test_channel_reset(mock_channel, chat_history):
|
||||
# Test to verify the reset functionality
|
||||
await mock_channel.receive(chat_history)
|
||||
assert len(mock_channel) == len(chat_history)
|
||||
assert len(mock_channel.messages) == len(chat_history)
|
||||
await mock_channel.reset()
|
||||
assert len(mock_channel) == 0
|
||||
assert len(mock_channel.messages) == 0
|
||||
|
||||
|
||||
async def test_receive_appends_history_correctly(mock_channel):
|
||||
"""Test that the receive method appends messages while ensuring they alternate in author role."""
|
||||
# Provide a list of messages with identical roles to see if placeholders are inserted
|
||||
incoming_messages = [
|
||||
ChatMessageContent(role=AuthorRole.USER, content="User message 1"),
|
||||
ChatMessageContent(role=AuthorRole.USER, content="User message 2"),
|
||||
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant message 1"),
|
||||
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant message 2"),
|
||||
]
|
||||
|
||||
await mock_channel.receive(incoming_messages)
|
||||
|
||||
# The final channel.messages should be:
|
||||
# user message 1, user placeholder, user message 2, assistant placeholder, assistant message 1,
|
||||
# assistant placeholder, assistant message 2
|
||||
expected_roles = [
|
||||
AuthorRole.USER,
|
||||
AuthorRole.ASSISTANT, # placeholder
|
||||
AuthorRole.USER,
|
||||
AuthorRole.ASSISTANT,
|
||||
AuthorRole.USER, # placeholder
|
||||
AuthorRole.ASSISTANT,
|
||||
]
|
||||
expected_contents = [
|
||||
"User message 1",
|
||||
BedrockAgentChannel.MESSAGE_PLACEHOLDER,
|
||||
"User message 2",
|
||||
"Assistant message 1",
|
||||
BedrockAgentChannel.MESSAGE_PLACEHOLDER,
|
||||
"Assistant message 2",
|
||||
]
|
||||
|
||||
assert len(mock_channel.messages) == len(expected_roles)
|
||||
for i, (msg, exp_role, exp_content) in enumerate(zip(mock_channel.messages, expected_roles, expected_contents)):
|
||||
assert msg.role == exp_role, f"Role mismatch at index {i}"
|
||||
assert msg.content == exp_content, f"Content mismatch at index {i}"
|
||||
|
||||
|
||||
async def test_invoke_raises_exception_for_non_bedrock_agent(mock_channel):
|
||||
"""Test invoke method raises AgentChatException if the agent provided is not a BedrockAgent."""
|
||||
# Place a message in the channel so it's not empty
|
||||
mock_channel.messages.append(ChatMessageContent(role=AuthorRole.USER, content="User message"))
|
||||
|
||||
# Create a dummy agent that is not BedrockAgent
|
||||
non_bedrock_agent = ConcreteAgent()
|
||||
|
||||
with pytest.raises(AgentChatException) as exc_info:
|
||||
_ = [msg async for msg in mock_channel.invoke(non_bedrock_agent)]
|
||||
|
||||
assert "Agent is not of the expected type" in str(exc_info.value)
|
||||
|
||||
|
||||
async def test_invoke_raises_exception_if_no_history(mock_channel, mock_agent):
|
||||
"""Test invoke method raises AgentChatException if no chat history is available."""
|
||||
with pytest.raises(AgentChatException) as exc_info:
|
||||
_ = [msg async for msg in mock_channel.invoke(mock_agent)]
|
||||
|
||||
assert "No chat history available" in str(exc_info.value)
|
||||
|
||||
|
||||
async def test_invoke_inserts_placeholders_when_history_needs_to_alternate(mock_channel, mock_agent):
|
||||
"""Test invoke ensures _ensure_history_alternates and _ensure_last_message_is_user are called."""
|
||||
# Put messages in the channel such that the last message is an assistant's
|
||||
mock_channel.messages.append(ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assistant 1"))
|
||||
|
||||
# Mock agent.invoke to return an async generator
|
||||
async def mock_invoke(messages: str, thread: AgentThread, sessionState=None, **kwargs):
|
||||
# We just yield one message as if the agent responded
|
||||
yield AgentResponseItem(
|
||||
message=ChatMessageContent(role=AuthorRole.ASSISTANT, content="Mock Agent Response"),
|
||||
thread=mock_channel.thread,
|
||||
)
|
||||
|
||||
mock_agent.invoke = mock_invoke
|
||||
|
||||
# Because the last message is from the assistant, we expect a placeholder user message to be appended
|
||||
# also the history might need to alternate.
|
||||
# But since there's only one message, there's nothing to fix except the last message is user.
|
||||
|
||||
# We will now add a user message so we do not get the "No chat history available" error
|
||||
mock_channel.messages.append(ChatMessageContent(role=AuthorRole.USER, content="User 1"))
|
||||
|
||||
# Now we do invoke
|
||||
outputs = [msg async for msg in mock_channel.invoke(mock_agent)]
|
||||
|
||||
# We'll check if the response is appended to channel.messages
|
||||
assert len(outputs) == 1
|
||||
assert outputs[0][0] is True, "Expected a user-facing response"
|
||||
agent_response = outputs[0][1]
|
||||
assert agent_response.content == "Mock Agent Response"
|
||||
|
||||
# The channel messages should now have 3 messages: the assistant, the user, and the new agent message
|
||||
assert len(mock_channel.messages) == 3
|
||||
assert mock_channel.messages[-1].role == AuthorRole.ASSISTANT
|
||||
assert mock_channel.messages[-1].content == "Mock Agent Response"
|
||||
|
||||
|
||||
async def test_invoke_stream_raises_error_for_non_bedrock_agent(mock_channel):
|
||||
"""Test invoke_stream raises AgentChatException if the agent provided is not a BedrockAgent."""
|
||||
mock_channel.messages.append(ChatMessageContent(role=AuthorRole.USER, content="User message"))
|
||||
|
||||
non_bedrock_agent = ConcreteAgent()
|
||||
|
||||
with pytest.raises(AgentChatException) as exc_info:
|
||||
_ = [msg async for msg in mock_channel.invoke_stream(non_bedrock_agent, [])]
|
||||
|
||||
assert "Agent is not of the expected type" in str(exc_info.value)
|
||||
|
||||
|
||||
async def test_invoke_stream_raises_no_chat_history(mock_channel, mock_agent):
|
||||
"""Test invoke_stream raises AgentChatException if no messages in the channel."""
|
||||
|
||||
with pytest.raises(AgentChatException) as exc_info:
|
||||
_ = [msg async for msg in mock_channel.invoke_stream(mock_agent, [])]
|
||||
|
||||
assert "No chat history available." in str(exc_info.value)
|
||||
|
||||
|
||||
async def test_invoke_stream_appends_response_message(mock_channel, mock_agent):
|
||||
"""Test invoke_stream properly yields streaming content and appends an aggregated message at the end."""
|
||||
# Put a user message in the channel so it won't raise No chat history
|
||||
mock_channel.messages.append(ChatMessageContent(role=AuthorRole.USER, content="Last user message"))
|
||||
|
||||
async def mock_invoke_stream(
|
||||
messages: str, thread: AgentThread, sessionState=None, **kwargs
|
||||
) -> AsyncIterable[StreamingChatMessageContent]:
|
||||
yield AgentResponseItem(
|
||||
message=StreamingChatMessageContent(
|
||||
role=AuthorRole.ASSISTANT,
|
||||
choice_index=0,
|
||||
content="Hello",
|
||||
),
|
||||
thread=mock_channel.thread,
|
||||
)
|
||||
yield AgentResponseItem(
|
||||
message=StreamingChatMessageContent(
|
||||
role=AuthorRole.ASSISTANT,
|
||||
choice_index=0,
|
||||
content=" World",
|
||||
),
|
||||
thread=mock_channel.thread,
|
||||
)
|
||||
|
||||
mock_agent.invoke_stream = mock_invoke_stream
|
||||
|
||||
# Check that we get the streamed messages and that the summarized message is appended afterward
|
||||
messages_param = [ChatMessageContent(role=AuthorRole.USER, content="Last user message")] # just to pass the param
|
||||
streamed_content = [msg async for msg in mock_channel.invoke_stream(mock_agent, messages_param)]
|
||||
|
||||
# We expect two streamed chunks: "Hello" and " World"
|
||||
assert len(streamed_content) == 2
|
||||
assert streamed_content[0].content == "Hello"
|
||||
assert streamed_content[1].content == " World"
|
||||
|
||||
# Then we expect the channel to append an aggregated ChatMessageContent with "Hello World"
|
||||
assert len(messages_param) == 2
|
||||
appended = messages_param[1]
|
||||
assert appended.role == AuthorRole.ASSISTANT
|
||||
assert appended.content == "Hello World"
|
||||
|
||||
|
||||
async def test_get_history(mock_channel, chat_history):
|
||||
"""Test get_history yields messages in reverse order."""
|
||||
|
||||
mock_channel.messages = chat_history
|
||||
|
||||
reversed_history = [msg async for msg in mock_channel.get_history()]
|
||||
|
||||
# Should be reversed
|
||||
assert reversed_history[0].content == "I'm good, thank you!"
|
||||
assert reversed_history[1].content == "How are you?"
|
||||
assert reversed_history[2].content == "Hello, User!"
|
||||
assert reversed_history[3].content == "Hello, Bedrock!"
|
||||
|
||||
|
||||
async def test_invoke_alternates_history_and_ensures_last_user_message(mock_channel, mock_agent):
|
||||
"""Test invoke method ensures history alternates and last message is user."""
|
||||
mock_channel.messages = [
|
||||
ChatMessageContent(role=AuthorRole.USER, content="User1"),
|
||||
ChatMessageContent(role=AuthorRole.USER, content="User2"),
|
||||
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assist1"),
|
||||
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assist2"),
|
||||
ChatMessageContent(role=AuthorRole.USER, content="User3"),
|
||||
ChatMessageContent(role=AuthorRole.USER, content="User4"),
|
||||
ChatMessageContent(role=AuthorRole.ASSISTANT, content="Assist3"),
|
||||
]
|
||||
|
||||
async for _, msg in mock_channel.invoke(mock_agent):
|
||||
pass
|
||||
|
||||
# let's define expected roles from that final structure:
|
||||
expected_roles = [
|
||||
AuthorRole.USER,
|
||||
AuthorRole.ASSISTANT, # placeholder
|
||||
AuthorRole.USER,
|
||||
AuthorRole.ASSISTANT,
|
||||
AuthorRole.USER, # placeholder
|
||||
AuthorRole.ASSISTANT,
|
||||
AuthorRole.USER,
|
||||
AuthorRole.ASSISTANT, # placeholder
|
||||
AuthorRole.USER,
|
||||
AuthorRole.ASSISTANT,
|
||||
AuthorRole.USER, # placeholder
|
||||
]
|
||||
expected_contents = [
|
||||
"User1",
|
||||
BedrockAgentChannel.MESSAGE_PLACEHOLDER,
|
||||
"User2",
|
||||
"Assist1",
|
||||
BedrockAgentChannel.MESSAGE_PLACEHOLDER,
|
||||
"Assist2",
|
||||
"User3",
|
||||
BedrockAgentChannel.MESSAGE_PLACEHOLDER,
|
||||
"User4",
|
||||
"Assist3",
|
||||
BedrockAgentChannel.MESSAGE_PLACEHOLDER,
|
||||
]
|
||||
|
||||
assert len(mock_channel.messages) == len(expected_roles)
|
||||
for i, (msg, exp_role, exp_content) in enumerate(zip(mock_channel.messages, expected_roles, expected_contents)):
|
||||
assert msg.role == exp_role, f"Role mismatch at index {i}. Got {msg.role}, expected {exp_role}"
|
||||
assert msg.content == exp_content, f"Content mismatch at index {i}. Got {msg.content}, expected {exp_content}"
|
||||
@@ -0,0 +1,27 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import pytest
|
||||
|
||||
from semantic_kernel.agents.bedrock.models.bedrock_agent_event_type import BedrockAgentEventType
|
||||
|
||||
|
||||
def test_bedrock_agent_event_type_values():
|
||||
"""Test case to verify the values of BedrockAgentEventType enum."""
|
||||
assert BedrockAgentEventType.CHUNK.value == "chunk"
|
||||
assert BedrockAgentEventType.TRACE.value == "trace"
|
||||
assert BedrockAgentEventType.RETURN_CONTROL.value == "returnControl"
|
||||
assert BedrockAgentEventType.FILES.value == "files"
|
||||
|
||||
|
||||
def test_bedrock_agent_event_type_enum():
|
||||
"""Test case to verify the type of BedrockAgentEventType enum members."""
|
||||
assert isinstance(BedrockAgentEventType.CHUNK, BedrockAgentEventType)
|
||||
assert isinstance(BedrockAgentEventType.TRACE, BedrockAgentEventType)
|
||||
assert isinstance(BedrockAgentEventType.RETURN_CONTROL, BedrockAgentEventType)
|
||||
assert isinstance(BedrockAgentEventType.FILES, BedrockAgentEventType)
|
||||
|
||||
|
||||
def test_bedrock_agent_event_type_invalid():
|
||||
"""Test case to verify error handling for invalid BedrockAgentEventType value."""
|
||||
with pytest.raises(ValueError):
|
||||
BedrockAgentEventType("invalid_value")
|
||||
@@ -0,0 +1,67 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from semantic_kernel.agents.bedrock.models.bedrock_agent_model import BedrockAgentModel
|
||||
|
||||
|
||||
def test_bedrock_agent_model_valid():
|
||||
"""Test case to verify the BedrockAgentModel with valid data."""
|
||||
model = BedrockAgentModel(
|
||||
agentId="test_id",
|
||||
agentName="test_name",
|
||||
agentVersion="1.0",
|
||||
foundationModel="test_model",
|
||||
agentStatus="CREATING",
|
||||
)
|
||||
assert model.agent_id == "test_id"
|
||||
assert model.agent_name == "test_name"
|
||||
assert model.agent_version == "1.0"
|
||||
assert model.foundation_model == "test_model"
|
||||
assert model.agent_status == "CREATING"
|
||||
|
||||
|
||||
def test_bedrock_agent_model_missing_agent_id():
|
||||
"""Test case to verify the BedrockAgentModel with missing agentId."""
|
||||
model = BedrockAgentModel(
|
||||
agentName="test_name",
|
||||
agentVersion="1.0",
|
||||
foundationModel="test_model",
|
||||
agentStatus="CREATING",
|
||||
)
|
||||
assert model.agent_id is None
|
||||
assert model.agent_name == "test_name"
|
||||
assert model.agent_version == "1.0"
|
||||
assert model.foundation_model == "test_model"
|
||||
assert model.agent_status == "CREATING"
|
||||
|
||||
|
||||
def test_bedrock_agent_model_missing_agent_name():
|
||||
"""Test case to verify the BedrockAgentModel with missing agentName."""
|
||||
model = BedrockAgentModel(
|
||||
agentId="test_id",
|
||||
agentVersion="1.0",
|
||||
foundationModel="test_model",
|
||||
agentStatus="CREATING",
|
||||
)
|
||||
assert model.agent_id == "test_id"
|
||||
assert model.agent_name is None
|
||||
assert model.agent_version == "1.0"
|
||||
assert model.foundation_model == "test_model"
|
||||
assert model.agent_status == "CREATING"
|
||||
|
||||
|
||||
def test_bedrock_agent_model_extra_field():
|
||||
"""Test case to verify the BedrockAgentModel with an extra field."""
|
||||
model = BedrockAgentModel(
|
||||
agentId="test_id",
|
||||
agentName="test_name",
|
||||
agentVersion="1.0",
|
||||
foundationModel="test_model",
|
||||
agentStatus="CREATING",
|
||||
extraField="extra_value",
|
||||
)
|
||||
assert model.agent_id == "test_id"
|
||||
assert model.agent_name == "test_name"
|
||||
assert model.agent_version == "1.0"
|
||||
assert model.foundation_model == "test_model"
|
||||
assert model.agent_status == "CREATING"
|
||||
assert model.extraField == "extra_value"
|
||||
@@ -0,0 +1,28 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from semantic_kernel.agents.bedrock.bedrock_agent_settings import BedrockAgentSettings
|
||||
|
||||
|
||||
def test_bedrock_agent_settings_from_env_vars(bedrock_agent_unit_test_env):
|
||||
"""Test loading BedrockAgentSettings from environment variables."""
|
||||
settings = BedrockAgentSettings(env_file_path="fake_path")
|
||||
|
||||
assert settings.agent_resource_role_arn == bedrock_agent_unit_test_env["BEDROCK_AGENT_AGENT_RESOURCE_ROLE_ARN"]
|
||||
assert settings.foundation_model == bedrock_agent_unit_test_env["BEDROCK_AGENT_FOUNDATION_MODEL"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exclude_list",
|
||||
[
|
||||
["BEDROCK_AGENT_AGENT_RESOURCE_ROLE_ARN"],
|
||||
["BEDROCK_AGENT_FOUNDATION_MODEL"],
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
def test_bedrock_agent_settings_from_env_vars_missing_required(bedrock_agent_unit_test_env):
|
||||
"""Test loading BedrockAgentSettings from environment variables with missing required fields."""
|
||||
with pytest.raises(ValidationError):
|
||||
BedrockAgentSettings(env_file_path="fake_path")
|
||||
@@ -0,0 +1,23 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import pytest
|
||||
|
||||
from semantic_kernel.agents.bedrock.models.bedrock_agent_status import BedrockAgentStatus
|
||||
|
||||
|
||||
def test_bedrock_agent_status_values():
|
||||
"""Test case to verify the values of BedrockAgentStatus enum."""
|
||||
assert BedrockAgentStatus.CREATING == "CREATING"
|
||||
assert BedrockAgentStatus.PREPARING == "PREPARING"
|
||||
assert BedrockAgentStatus.PREPARED == "PREPARED"
|
||||
assert BedrockAgentStatus.NOT_PREPARED == "NOT_PREPARED"
|
||||
assert BedrockAgentStatus.DELETING == "DELETING"
|
||||
assert BedrockAgentStatus.FAILED == "FAILED"
|
||||
assert BedrockAgentStatus.VERSIONING == "VERSIONING"
|
||||
assert BedrockAgentStatus.UPDATING == "UPDATING"
|
||||
|
||||
|
||||
def test_bedrock_agent_status_invalid_value():
|
||||
"""Test case to verify error handling for invalid BedrockAgentStatus value."""
|
||||
with pytest.raises(ValueError):
|
||||
BedrockAgentStatus("INVALID_STATUS")
|
||||
Reference in New Issue
Block a user