chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from collections.abc import AsyncIterable
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from semantic_kernel.agents import Agent
|
||||
from semantic_kernel.agents.channels.agent_channel import AgentChannel
|
||||
from semantic_kernel.contents.chat_message_content import ChatMessageContent
|
||||
from semantic_kernel.contents.utils.author_role import AuthorRole
|
||||
|
||||
|
||||
class MockAgentChannel(AgentChannel):
|
||||
async def receive(self, history: list[ChatMessageContent]) -> None:
|
||||
pass
|
||||
|
||||
async def invoke(self, agent: "Agent") -> AsyncIterable[ChatMessageContent]:
|
||||
yield ChatMessageContent(role=AuthorRole.SYSTEM, content="test message")
|
||||
|
||||
async def get_history(self) -> AsyncIterable[ChatMessageContent]:
|
||||
yield ChatMessageContent(role=AuthorRole.SYSTEM, content="test history message")
|
||||
|
||||
|
||||
async def test_receive():
|
||||
mock_channel = AsyncMock(spec=MockAgentChannel)
|
||||
|
||||
history = [
|
||||
ChatMessageContent(role=AuthorRole.SYSTEM, content="test message 1"),
|
||||
ChatMessageContent(role=AuthorRole.USER, content="test message 2"),
|
||||
]
|
||||
|
||||
await mock_channel.receive(history)
|
||||
mock_channel.receive.assert_called_once_with(history)
|
||||
|
||||
|
||||
async def test_invoke():
|
||||
mock_channel = AsyncMock(spec=MockAgentChannel)
|
||||
agent = AsyncMock()
|
||||
|
||||
async def async_generator():
|
||||
yield ChatMessageContent(role=AuthorRole.SYSTEM, content="test message")
|
||||
|
||||
mock_channel.invoke.return_value = async_generator()
|
||||
|
||||
async for message in mock_channel.invoke(agent):
|
||||
assert message.content == "test message"
|
||||
mock_channel.invoke.assert_called_once_with(agent)
|
||||
|
||||
|
||||
async def test_get_history():
|
||||
mock_channel = AsyncMock(spec=MockAgentChannel)
|
||||
|
||||
async def async_generator():
|
||||
yield ChatMessageContent(role=AuthorRole.SYSTEM, content="test history message")
|
||||
|
||||
mock_channel.get_history.return_value = async_generator()
|
||||
|
||||
async for message in mock_channel.get_history():
|
||||
assert message.content == "test history message"
|
||||
mock_channel.get_history.assert_called_once()
|
||||
@@ -0,0 +1,245 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from unittest import mock
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from semantic_kernel.agents.channels.agent_channel import AgentChannel
|
||||
from semantic_kernel.agents.group_chat.agent_chat import AgentChat
|
||||
from semantic_kernel.agents.group_chat.broadcast_queue import ChannelReference
|
||||
from semantic_kernel.contents.chat_message_content import ChatMessageContent
|
||||
from semantic_kernel.contents.utils.author_role import AuthorRole
|
||||
from semantic_kernel.exceptions.agent_exceptions import AgentChatException
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def agent_chat():
|
||||
return AgentChat()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def agent():
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.name = "TestAgent"
|
||||
return mock_agent
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def chat_message():
|
||||
mock_chat_message = MagicMock(spec=ChatMessageContent)
|
||||
mock_chat_message.role = "user"
|
||||
return mock_chat_message
|
||||
|
||||
|
||||
async def test_set_activity_or_throw_when_inactive(agent_chat):
|
||||
agent_chat._is_active = False
|
||||
agent_chat.set_activity_or_throw()
|
||||
assert agent_chat.is_active
|
||||
|
||||
|
||||
async def test_set_activity_or_throw_when_active(agent_chat):
|
||||
agent_chat._is_active = True
|
||||
with pytest.raises(Exception, match="Unable to proceed while another agent is active."):
|
||||
agent_chat.set_activity_or_throw()
|
||||
|
||||
|
||||
async def test_clear_activity_signal(agent_chat):
|
||||
agent_chat._is_active = True
|
||||
agent_chat.clear_activity_signal()
|
||||
assert not agent_chat.is_active
|
||||
|
||||
|
||||
async def test_get_messages_in_descending_order(agent_chat, chat_message):
|
||||
agent_chat.history.messages = [chat_message, chat_message, chat_message]
|
||||
messages = []
|
||||
async for message in agent_chat.get_messages_in_descending_order():
|
||||
messages.append(message)
|
||||
assert len(messages) == 3
|
||||
|
||||
|
||||
async def test_get_chat_messages_without_agent(agent_chat, chat_message):
|
||||
agent_chat.history.messages = [chat_message]
|
||||
with patch(
|
||||
"semantic_kernel.agents.group_chat.agent_chat.AgentChat.get_messages_in_descending_order",
|
||||
return_value=AsyncMock(),
|
||||
) as mock_get_messages:
|
||||
async for _ in agent_chat.get_chat_messages():
|
||||
pass
|
||||
mock_get_messages.assert_called_once()
|
||||
|
||||
|
||||
async def test_get_chat_messages_with_agent(agent_chat, agent, chat_message):
|
||||
agent_chat.channel_map[agent] = "test_channel"
|
||||
|
||||
mock_channel = mock.MagicMock(spec=AgentChannel)
|
||||
agent_chat.agent_channels["test_channel"] = mock_channel
|
||||
|
||||
with (
|
||||
patch("semantic_kernel.agents.group_chat.agent_chat.AgentChat._get_agent_hash", return_value="test_channel"),
|
||||
patch("semantic_kernel.agents.group_chat.agent_chat.AgentChat._synchronize_channel", return_value=mock_channel),
|
||||
patch.object(mock_channel, "get_history", return_value=AsyncMock()),
|
||||
):
|
||||
async for _ in agent_chat.get_chat_messages(agent):
|
||||
pass
|
||||
|
||||
|
||||
async def test_add_chat_message(agent_chat, chat_message):
|
||||
with patch(
|
||||
"semantic_kernel.agents.group_chat.agent_chat.AgentChat.add_chat_messages",
|
||||
return_value=AsyncMock(),
|
||||
) as mock_add_chat_messages:
|
||||
await agent_chat.add_chat_message(chat_message)
|
||||
mock_add_chat_messages.assert_called_once_with([chat_message])
|
||||
|
||||
|
||||
async def test_add_chat_messages(agent_chat, chat_message):
|
||||
with patch("semantic_kernel.agents.group_chat.broadcast_queue.BroadcastQueue.enqueue", return_value=AsyncMock()):
|
||||
await agent_chat.add_chat_messages([chat_message])
|
||||
assert chat_message in agent_chat.history.messages
|
||||
|
||||
|
||||
async def test_invoke_agent(agent_chat, agent, chat_message):
|
||||
mock_channel = mock.MagicMock(spec=AgentChannel)
|
||||
|
||||
async def mock_invoke(*args, **kwargs):
|
||||
yield True, chat_message
|
||||
|
||||
mock_channel.invoke.side_effect = mock_invoke
|
||||
|
||||
with (
|
||||
patch(
|
||||
"semantic_kernel.agents.group_chat.agent_chat.AgentChat._get_or_create_channel", return_value=mock_channel
|
||||
),
|
||||
patch(
|
||||
"semantic_kernel.agents.group_chat.broadcast_queue.BroadcastQueue.enqueue",
|
||||
return_value=AsyncMock(),
|
||||
),
|
||||
):
|
||||
async for _ in agent_chat.invoke_agent(agent):
|
||||
pass
|
||||
|
||||
mock_channel.invoke.assert_called_once_with(agent)
|
||||
await agent_chat.reset()
|
||||
|
||||
|
||||
async def test_invoke_streaming_agent(agent_chat, agent, chat_message):
|
||||
mock_channel = mock.MagicMock(spec=AgentChannel)
|
||||
|
||||
async def mock_invoke(*args, **kwargs):
|
||||
yield chat_message
|
||||
|
||||
mock_channel.invoke_stream.side_effect = mock_invoke
|
||||
|
||||
with (
|
||||
patch(
|
||||
"semantic_kernel.agents.group_chat.agent_chat.AgentChat._get_or_create_channel", return_value=mock_channel
|
||||
),
|
||||
patch(
|
||||
"semantic_kernel.agents.group_chat.broadcast_queue.BroadcastQueue.enqueue",
|
||||
return_value=AsyncMock(),
|
||||
),
|
||||
):
|
||||
async for _ in agent_chat.invoke_agent_stream(agent):
|
||||
pass
|
||||
|
||||
mock_channel.invoke_stream.assert_called_once_with(agent, [])
|
||||
await agent_chat.reset()
|
||||
|
||||
|
||||
async def test_synchronize_channel_with_existing_channel(agent_chat):
|
||||
mock_channel = MagicMock(spec=AgentChannel)
|
||||
channel_key = "test_channel_key"
|
||||
agent_chat.agent_channels[channel_key] = mock_channel
|
||||
|
||||
with patch(
|
||||
"semantic_kernel.agents.group_chat.broadcast_queue.BroadcastQueue.ensure_synchronized", return_value=AsyncMock()
|
||||
) as mock_ensure_synchronized:
|
||||
result = await agent_chat._synchronize_channel(channel_key)
|
||||
|
||||
assert result == mock_channel
|
||||
mock_ensure_synchronized.assert_called_once_with(ChannelReference(channel=mock_channel, hash=channel_key))
|
||||
|
||||
|
||||
async def test_synchronize_channel_with_nonexistent_channel(agent_chat):
|
||||
channel_key = "test_channel_key"
|
||||
|
||||
result = await agent_chat._synchronize_channel(channel_key)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_get_agent_hash_with_existing_hash(agent_chat, agent):
|
||||
expected_hash = "existing_hash"
|
||||
agent_chat.channel_map[agent] = expected_hash
|
||||
|
||||
result = agent_chat._get_agent_hash(agent)
|
||||
|
||||
assert result == expected_hash
|
||||
|
||||
|
||||
def test_get_agent_hash_generates_new_hash(agent_chat, agent):
|
||||
expected_hash = "new_hash"
|
||||
agent.get_channel_keys = MagicMock(return_value=["key1", "key2"])
|
||||
|
||||
with patch(
|
||||
"semantic_kernel.agents.group_chat.agent_chat.KeyEncoder.generate_hash", return_value=expected_hash
|
||||
) as mock_generate_hash:
|
||||
result = agent_chat._get_agent_hash(agent)
|
||||
|
||||
assert result == expected_hash
|
||||
mock_generate_hash.assert_called_once_with(["key1", "key2"])
|
||||
assert agent_chat.channel_map[agent] == expected_hash
|
||||
|
||||
|
||||
async def test_add_chat_messages_throws_exception_for_system_role(agent_chat):
|
||||
system_message = MagicMock(spec=ChatMessageContent)
|
||||
system_message.role = AuthorRole.SYSTEM
|
||||
|
||||
with pytest.raises(AgentChatException, match="System messages cannot be added to the chat history."):
|
||||
await agent_chat.add_chat_messages([system_message])
|
||||
|
||||
|
||||
async def test_get_or_create_channel_creates_new_channel(agent_chat, agent):
|
||||
agent_chat.history.messages = [MagicMock(spec=ChatMessageContent)]
|
||||
channel_key = "test_channel_key"
|
||||
mock_channel = AsyncMock(spec=AgentChannel)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"semantic_kernel.agents.group_chat.agent_chat.AgentChat._get_agent_hash", return_value=channel_key
|
||||
) as mock_get_agent_hash,
|
||||
patch(
|
||||
"semantic_kernel.agents.group_chat.agent_chat.AgentChat._synchronize_channel", return_value=None
|
||||
) as mock_synchronize_channel,
|
||||
):
|
||||
agent.create_channel = AsyncMock(return_value=mock_channel)
|
||||
with patch.object(mock_channel, "receive", return_value=AsyncMock()) as mock_receive:
|
||||
result = await agent_chat._get_or_create_channel(agent)
|
||||
|
||||
assert result == mock_channel
|
||||
mock_get_agent_hash.assert_called_once_with(agent)
|
||||
mock_synchronize_channel.assert_called_once_with(channel_key)
|
||||
agent.create_channel.assert_called_once()
|
||||
mock_receive.assert_called_once_with(agent_chat.history.messages)
|
||||
assert agent_chat.agent_channels[channel_key] == mock_channel
|
||||
|
||||
|
||||
async def test_get_or_create_channel_reuses_existing_channel(agent_chat, agent):
|
||||
channel_key = "test_channel_key"
|
||||
mock_channel = MagicMock(spec=AgentChannel)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"semantic_kernel.agents.group_chat.agent_chat.AgentChat._get_agent_hash", return_value=channel_key
|
||||
) as mock_get_agent_hash,
|
||||
patch(
|
||||
"semantic_kernel.agents.group_chat.agent_chat.AgentChat._synchronize_channel", return_value=mock_channel
|
||||
) as mock_synchronize_channel,
|
||||
):
|
||||
result = await agent_chat._get_or_create_channel(agent)
|
||||
|
||||
assert result == mock_channel
|
||||
mock_get_agent_hash.assert_called_once_with(agent)
|
||||
mock_synchronize_channel.assert_called_once_with(channel_key)
|
||||
agent.create_channel.assert_not_called()
|
||||
@@ -0,0 +1,27 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import base64
|
||||
from hashlib import sha256
|
||||
|
||||
import pytest
|
||||
|
||||
from semantic_kernel.agents.group_chat.agent_chat_utils import KeyEncoder
|
||||
from semantic_kernel.exceptions.agent_exceptions import AgentExecutionException
|
||||
|
||||
|
||||
def test_generate_hash_valid_keys():
|
||||
keys = ["key1", "key2", "key3"]
|
||||
expected_joined_keys = ":".join(keys).encode("utf-8")
|
||||
expected_hash = sha256(expected_joined_keys).digest()
|
||||
expected_base64 = base64.b64encode(expected_hash).decode("utf-8")
|
||||
|
||||
result = KeyEncoder.generate_hash(keys)
|
||||
|
||||
assert result == expected_base64
|
||||
|
||||
|
||||
def test_generate_hash_empty_keys():
|
||||
with pytest.raises(
|
||||
AgentExecutionException, match="Channel Keys must not be empty. Unable to generate channel hash."
|
||||
):
|
||||
KeyEncoder.generate_hash([])
|
||||
@@ -0,0 +1,341 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from unittest import mock
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from semantic_kernel.agents.agent import Agent
|
||||
from semantic_kernel.agents.group_chat.agent_chat import AgentChat
|
||||
from semantic_kernel.agents.group_chat.agent_group_chat import AgentGroupChat
|
||||
from semantic_kernel.agents.strategies.selection.selection_strategy import SelectionStrategy
|
||||
from semantic_kernel.agents.strategies.selection.sequential_selection_strategy import SequentialSelectionStrategy
|
||||
from semantic_kernel.agents.strategies.termination.default_termination_strategy import DefaultTerminationStrategy
|
||||
from semantic_kernel.agents.strategies.termination.termination_strategy import TerminationStrategy
|
||||
from semantic_kernel.contents.utils.author_role import AuthorRole
|
||||
from semantic_kernel.exceptions.agent_exceptions import AgentChatException
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def agents():
|
||||
"""Fixture that provides a list of mock agents."""
|
||||
return [MagicMock(spec=Agent, id=f"agent-{i}") for i in range(3)]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def termination_strategy():
|
||||
"""Fixture that provides a mock termination strategy."""
|
||||
return AsyncMock(spec=TerminationStrategy)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def selection_strategy():
|
||||
"""Fixture that provides a mock selection strategy."""
|
||||
return AsyncMock(spec=SelectionStrategy)
|
||||
|
||||
|
||||
# region Non-Streaming
|
||||
|
||||
|
||||
def test_agent_group_chat_initialization(agents, termination_strategy, selection_strategy):
|
||||
group_chat = AgentGroupChat(
|
||||
agents=agents, termination_strategy=termination_strategy, selection_strategy=selection_strategy
|
||||
)
|
||||
|
||||
assert group_chat.agents == agents
|
||||
assert group_chat.agent_ids == {agent.id for agent in agents}
|
||||
assert group_chat.termination_strategy == termination_strategy
|
||||
assert group_chat.selection_strategy == selection_strategy
|
||||
|
||||
|
||||
def test_agent_group_chat_initialization_defaults():
|
||||
group_chat = AgentGroupChat()
|
||||
|
||||
assert group_chat.agents == []
|
||||
assert group_chat.agent_ids == set()
|
||||
assert isinstance(group_chat.termination_strategy, DefaultTerminationStrategy)
|
||||
assert isinstance(group_chat.selection_strategy, SequentialSelectionStrategy)
|
||||
|
||||
|
||||
def test_add_agent(agents):
|
||||
group_chat = AgentGroupChat()
|
||||
|
||||
group_chat.add_agent(agents[0])
|
||||
|
||||
assert agents[0] in group_chat.agents
|
||||
assert agents[0].id in group_chat.agent_ids
|
||||
|
||||
|
||||
def test_add_duplicate_agent(agents):
|
||||
group_chat = AgentGroupChat(agents=[agents[0]])
|
||||
|
||||
group_chat.add_agent(agents[0])
|
||||
|
||||
assert len(group_chat.agents) == 1
|
||||
assert len(group_chat.agent_ids) == 1
|
||||
|
||||
|
||||
async def test_invoke_single_turn(agents, termination_strategy):
|
||||
group_chat = AgentGroupChat(termination_strategy=termination_strategy)
|
||||
|
||||
async def mock_invoke(agent, is_joining=True):
|
||||
yield MagicMock(role=AuthorRole.ASSISTANT)
|
||||
|
||||
with mock.patch.object(AgentGroupChat, "invoke", side_effect=mock_invoke):
|
||||
termination_strategy.should_terminate.return_value = False
|
||||
|
||||
async for message in group_chat.invoke_single_turn(agents[0]):
|
||||
assert message.role == AuthorRole.ASSISTANT
|
||||
|
||||
termination_strategy.should_terminate.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_invoke_single_turn_sets_complete(agents, termination_strategy):
|
||||
group_chat = AgentGroupChat(termination_strategy=termination_strategy)
|
||||
|
||||
async def mock_invoke(agent, is_joining=True):
|
||||
yield MagicMock(role=AuthorRole.ASSISTANT)
|
||||
|
||||
with mock.patch.object(AgentGroupChat, "invoke", side_effect=mock_invoke):
|
||||
termination_strategy.should_terminate.return_value = True
|
||||
|
||||
async for _ in group_chat.invoke_single_turn(agents[0]):
|
||||
pass
|
||||
|
||||
assert group_chat.is_complete is True
|
||||
termination_strategy.should_terminate.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_invoke_with_agent_joining(agents, termination_strategy):
|
||||
for agent in agents:
|
||||
agent.name = f"Agent {agent.id}"
|
||||
agent.id = f"agent-{agent.id}"
|
||||
|
||||
group_chat = AgentGroupChat(termination_strategy=termination_strategy)
|
||||
|
||||
with (
|
||||
mock.patch.object(AgentGroupChat, "add_agent", autospec=True) as mock_add_agent,
|
||||
mock.patch.object(AgentChat, "invoke_agent", autospec=True) as mock_invoke_agent,
|
||||
):
|
||||
|
||||
async def mock_invoke_gen(*args, **kwargs):
|
||||
yield MagicMock(role=AuthorRole.ASSISTANT)
|
||||
|
||||
mock_invoke_agent.side_effect = mock_invoke_gen
|
||||
|
||||
async for _ in group_chat.invoke(agents[0], is_joining=True):
|
||||
pass
|
||||
|
||||
mock_add_agent.assert_called_once_with(group_chat, agents[0])
|
||||
|
||||
|
||||
async def test_invoke_with_complete_chat(agents, termination_strategy):
|
||||
termination_strategy.automatic_reset = False
|
||||
group_chat = AgentGroupChat(agents=agents, termination_strategy=termination_strategy)
|
||||
group_chat.is_complete = True
|
||||
|
||||
with pytest.raises(AgentChatException, match="Chat is already complete"):
|
||||
async for _ in group_chat.invoke():
|
||||
pass
|
||||
|
||||
|
||||
async def test_invoke_agent_with_none_defined_errors(agents):
|
||||
group_chat = AgentGroupChat()
|
||||
|
||||
with pytest.raises(AgentChatException, match="No agents are available"):
|
||||
async for _ in group_chat.invoke():
|
||||
pass
|
||||
|
||||
|
||||
async def test_invoke_selection_strategy_error(agents, selection_strategy):
|
||||
group_chat = AgentGroupChat(agents=agents, selection_strategy=selection_strategy)
|
||||
|
||||
selection_strategy.next.side_effect = Exception("Selection failed")
|
||||
|
||||
with pytest.raises(AgentChatException, match="Failed to select agent"):
|
||||
async for _ in group_chat.invoke():
|
||||
pass
|
||||
|
||||
|
||||
async def test_invoke_iterations(agents, termination_strategy, selection_strategy):
|
||||
for agent in agents:
|
||||
agent.name = f"Agent {agent.id}"
|
||||
agent.id = f"agent-{agent.id}"
|
||||
|
||||
termination_strategy.maximum_iterations = 2
|
||||
|
||||
group_chat = AgentGroupChat(
|
||||
agents=agents, termination_strategy=termination_strategy, selection_strategy=selection_strategy
|
||||
)
|
||||
|
||||
selection_strategy.next.side_effect = lambda agents, history: agents[0]
|
||||
|
||||
async def mock_invoke_agent(*args, **kwargs):
|
||||
yield MagicMock(role=AuthorRole.ASSISTANT)
|
||||
|
||||
with mock.patch.object(AgentChat, "invoke_agent", side_effect=mock_invoke_agent):
|
||||
termination_strategy.should_terminate.return_value = False
|
||||
|
||||
iteration_count = 0
|
||||
async for _ in group_chat.invoke():
|
||||
iteration_count += 1
|
||||
|
||||
assert iteration_count == 2
|
||||
|
||||
|
||||
async def test_invoke_is_complete_then_reset(agents, termination_strategy, selection_strategy):
|
||||
for agent in agents:
|
||||
agent.name = f"Agent {agent.id}"
|
||||
agent.id = f"agent-{agent.id}"
|
||||
|
||||
termination_strategy.maximum_iterations = 2
|
||||
termination_strategy.automatic_reset = True
|
||||
|
||||
group_chat = AgentGroupChat(
|
||||
agents=agents, termination_strategy=termination_strategy, selection_strategy=selection_strategy
|
||||
)
|
||||
|
||||
group_chat.is_complete = True
|
||||
|
||||
selection_strategy.next.side_effect = lambda agents, history: agents[0]
|
||||
|
||||
async def mock_invoke_agent(*args, **kwargs):
|
||||
yield MagicMock(role=AuthorRole.ASSISTANT)
|
||||
|
||||
with mock.patch.object(AgentChat, "invoke_agent", side_effect=mock_invoke_agent):
|
||||
termination_strategy.should_terminate.return_value = False
|
||||
|
||||
iteration_count = 0
|
||||
async for _ in group_chat.invoke():
|
||||
iteration_count += 1
|
||||
|
||||
assert iteration_count == 2
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Streaming
|
||||
|
||||
|
||||
async def test_invoke_streaming_single_turn(agents, termination_strategy):
|
||||
group_chat = AgentGroupChat(termination_strategy=termination_strategy)
|
||||
|
||||
async def mock_invoke(agent, is_joining=True):
|
||||
yield MagicMock(role=AuthorRole.ASSISTANT)
|
||||
|
||||
with mock.patch.object(AgentGroupChat, "invoke_stream", side_effect=mock_invoke):
|
||||
termination_strategy.should_terminate.return_value = False
|
||||
|
||||
async for message in group_chat.invoke_stream_single_turn(agents[0]):
|
||||
assert message.role == AuthorRole.ASSISTANT
|
||||
|
||||
termination_strategy.should_terminate.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_invoke_stream_with_agent_joining(agents, termination_strategy):
|
||||
for agent in agents:
|
||||
agent.name = f"Agent {agent.id}"
|
||||
agent.id = f"agent-{agent.id}"
|
||||
|
||||
group_chat = AgentGroupChat(termination_strategy=termination_strategy)
|
||||
|
||||
with (
|
||||
mock.patch.object(AgentGroupChat, "add_agent", autospec=True) as mock_add_agent,
|
||||
mock.patch.object(AgentChat, "invoke_agent_stream", autospec=True) as mock_invoke_agent,
|
||||
):
|
||||
|
||||
async def mock_invoke_gen(*args, **kwargs):
|
||||
yield MagicMock(role=AuthorRole.ASSISTANT)
|
||||
|
||||
mock_invoke_agent.side_effect = mock_invoke_gen
|
||||
|
||||
async for _ in group_chat.invoke_stream(agents[0], is_joining=True):
|
||||
pass
|
||||
|
||||
mock_add_agent.assert_called_once_with(group_chat, agents[0])
|
||||
|
||||
|
||||
async def test_invoke_stream_with_complete_chat(agents, termination_strategy):
|
||||
termination_strategy.automatic_reset = False
|
||||
group_chat = AgentGroupChat(agents=agents, termination_strategy=termination_strategy)
|
||||
group_chat.is_complete = True
|
||||
|
||||
with pytest.raises(AgentChatException, match="Chat is already complete"):
|
||||
async for _ in group_chat.invoke_stream():
|
||||
pass
|
||||
|
||||
|
||||
async def test_invoke_stream_selection_strategy_error(agents, selection_strategy):
|
||||
group_chat = AgentGroupChat(agents=agents, selection_strategy=selection_strategy)
|
||||
|
||||
selection_strategy.next.side_effect = Exception("Selection failed")
|
||||
|
||||
with pytest.raises(AgentChatException, match="Failed to select agent"):
|
||||
async for _ in group_chat.invoke_stream():
|
||||
pass
|
||||
|
||||
|
||||
async def test_invoke_stream_iterations(agents, termination_strategy, selection_strategy):
|
||||
for agent in agents:
|
||||
agent.name = f"Agent {agent.id}"
|
||||
agent.id = f"agent-{agent.id}"
|
||||
|
||||
termination_strategy.maximum_iterations = 2
|
||||
|
||||
group_chat = AgentGroupChat(
|
||||
agents=agents, termination_strategy=termination_strategy, selection_strategy=selection_strategy
|
||||
)
|
||||
|
||||
selection_strategy.next.side_effect = lambda agents, history: agents[0]
|
||||
|
||||
async def mock_invoke_agent(*args, **kwargs):
|
||||
yield MagicMock(role=AuthorRole.ASSISTANT)
|
||||
|
||||
with mock.patch.object(AgentChat, "invoke_agent_stream", side_effect=mock_invoke_agent):
|
||||
termination_strategy.should_terminate.return_value = False
|
||||
|
||||
iteration_count = 0
|
||||
async for _ in group_chat.invoke_stream():
|
||||
iteration_count += 1
|
||||
|
||||
assert iteration_count == 2
|
||||
|
||||
|
||||
async def test_invoke_stream_is_complete_then_reset(agents, termination_strategy, selection_strategy):
|
||||
for agent in agents:
|
||||
agent.name = f"Agent {agent.id}"
|
||||
agent.id = f"agent-{agent.id}"
|
||||
|
||||
termination_strategy.maximum_iterations = 2
|
||||
termination_strategy.automatic_reset = True
|
||||
|
||||
group_chat = AgentGroupChat(
|
||||
agents=agents, termination_strategy=termination_strategy, selection_strategy=selection_strategy
|
||||
)
|
||||
|
||||
group_chat.is_complete = True
|
||||
|
||||
selection_strategy.next.side_effect = lambda agents, history: agents[0]
|
||||
|
||||
async def mock_invoke_agent(*args, **kwargs):
|
||||
yield MagicMock(role=AuthorRole.ASSISTANT)
|
||||
|
||||
with mock.patch.object(AgentChat, "invoke_agent_stream", side_effect=mock_invoke_agent):
|
||||
termination_strategy.should_terminate.return_value = False
|
||||
|
||||
iteration_count = 0
|
||||
async for _ in group_chat.invoke_stream():
|
||||
iteration_count += 1
|
||||
|
||||
assert iteration_count == 2
|
||||
|
||||
|
||||
async def test_invoke_streaming_agent_with_none_defined_errors(agents):
|
||||
group_chat = AgentGroupChat()
|
||||
|
||||
with pytest.raises(AgentChatException, match="No agents are available"):
|
||||
async for _ in group_chat.invoke_stream():
|
||||
pass
|
||||
|
||||
|
||||
# endregion
|
||||
@@ -0,0 +1,172 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from semantic_kernel.agents.channels.agent_channel import AgentChannel
|
||||
from semantic_kernel.agents.group_chat.broadcast_queue import BroadcastQueue, ChannelReference, QueueReference
|
||||
from semantic_kernel.contents.chat_message_content import ChatMessageContent
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def channel_ref():
|
||||
"""Fixture that provides a mock ChannelReference."""
|
||||
mock_channel = AsyncMock(spec=AgentChannel)
|
||||
return ChannelReference(channel=mock_channel, hash="test-hash")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def message():
|
||||
"""Fixture that provides a mock ChatMessageContent."""
|
||||
return MagicMock(spec=ChatMessageContent)
|
||||
|
||||
|
||||
# region QueueReference Tests
|
||||
|
||||
|
||||
def test_queue_reference_is_empty_true():
|
||||
queue_ref = QueueReference()
|
||||
assert queue_ref.is_empty is True
|
||||
|
||||
|
||||
def test_queue_reference_is_empty_false():
|
||||
queue_ref = QueueReference()
|
||||
queue_ref.queue.append(MagicMock())
|
||||
assert queue_ref.is_empty is False
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region BroadcastQueue Tests
|
||||
|
||||
|
||||
async def test_enqueue_new_channel(channel_ref, message):
|
||||
broadcast_queue = BroadcastQueue()
|
||||
|
||||
await broadcast_queue.enqueue([channel_ref], [message])
|
||||
|
||||
assert channel_ref.hash in broadcast_queue.queues
|
||||
queue_ref = broadcast_queue.queues[channel_ref.hash]
|
||||
assert queue_ref.queue[0] == [message]
|
||||
assert queue_ref.receive_task is not None
|
||||
assert not queue_ref.receive_task.done()
|
||||
|
||||
|
||||
async def test_enqueue_existing_channel(channel_ref, message):
|
||||
broadcast_queue = BroadcastQueue()
|
||||
|
||||
await broadcast_queue.enqueue([channel_ref], [message])
|
||||
|
||||
await broadcast_queue.enqueue([channel_ref], [message])
|
||||
|
||||
queue_ref = broadcast_queue.queues[channel_ref.hash]
|
||||
assert len(queue_ref.queue) == 2
|
||||
assert queue_ref.queue[1] == [message]
|
||||
assert queue_ref.receive_task is not None
|
||||
assert not queue_ref.receive_task.done()
|
||||
|
||||
|
||||
async def test_ensure_synchronized_channel_empty(channel_ref):
|
||||
broadcast_queue = BroadcastQueue()
|
||||
|
||||
await broadcast_queue.ensure_synchronized(channel_ref)
|
||||
assert channel_ref.hash not in broadcast_queue.queues
|
||||
|
||||
|
||||
async def test_ensure_synchronized_with_messages(channel_ref, message):
|
||||
broadcast_queue = BroadcastQueue()
|
||||
|
||||
await broadcast_queue.enqueue([channel_ref], [message])
|
||||
|
||||
await broadcast_queue.ensure_synchronized(channel_ref)
|
||||
|
||||
queue_ref = broadcast_queue.queues[channel_ref.hash]
|
||||
assert queue_ref.is_empty is True
|
||||
|
||||
|
||||
async def test_ensure_synchronized_with_failure(channel_ref, message):
|
||||
broadcast_queue = BroadcastQueue()
|
||||
|
||||
await broadcast_queue.enqueue([channel_ref], [message])
|
||||
|
||||
queue_ref = broadcast_queue.queues[channel_ref.hash]
|
||||
queue_ref.receive_failure = Exception("Simulated failure")
|
||||
|
||||
with pytest.raises(Exception, match="Unexpected failure broadcasting to channel"):
|
||||
await broadcast_queue.ensure_synchronized(channel_ref)
|
||||
|
||||
assert queue_ref.receive_failure is None
|
||||
|
||||
|
||||
async def test_ensure_synchronized_creates_new_task(channel_ref, message):
|
||||
broadcast_queue = BroadcastQueue()
|
||||
|
||||
await broadcast_queue.enqueue([channel_ref], [message])
|
||||
|
||||
queue_ref = broadcast_queue.queues[channel_ref.hash]
|
||||
|
||||
queue_ref.receive_task = None
|
||||
|
||||
with patch(
|
||||
"semantic_kernel.agents.group_chat.broadcast_queue.BroadcastQueue.receive", new_callable=AsyncMock
|
||||
) as mock_receive:
|
||||
mock_receive.return_value = await asyncio.sleep(0.1)
|
||||
|
||||
await broadcast_queue.ensure_synchronized(channel_ref)
|
||||
|
||||
assert queue_ref.receive_task is None
|
||||
|
||||
|
||||
async def test_receive_processes_queue(channel_ref, message):
|
||||
broadcast_queue = BroadcastQueue()
|
||||
|
||||
await broadcast_queue.enqueue([channel_ref], [message])
|
||||
|
||||
queue_ref = broadcast_queue.queues[channel_ref.hash]
|
||||
|
||||
await broadcast_queue.receive(channel_ref, queue_ref)
|
||||
|
||||
assert queue_ref.is_empty is True
|
||||
|
||||
assert channel_ref.channel.receive.await_count >= 1
|
||||
channel_ref.channel.receive.assert_any_await([message])
|
||||
|
||||
|
||||
async def test_receive_handles_failure(channel_ref, message):
|
||||
broadcast_queue = BroadcastQueue()
|
||||
|
||||
await broadcast_queue.enqueue([channel_ref], [message])
|
||||
|
||||
channel_ref.channel.receive.side_effect = Exception("Simulated failure")
|
||||
|
||||
queue_ref = broadcast_queue.queues[channel_ref.hash]
|
||||
|
||||
await broadcast_queue.receive(channel_ref, queue_ref)
|
||||
|
||||
assert queue_ref.receive_failure is not None
|
||||
assert str(queue_ref.receive_failure) == "Simulated failure"
|
||||
|
||||
|
||||
async def test_receive_breaks_when_queue_is_empty(channel_ref, message):
|
||||
broadcast_queue = BroadcastQueue()
|
||||
|
||||
await broadcast_queue.enqueue([channel_ref], [message])
|
||||
|
||||
queue_ref = broadcast_queue.queues[channel_ref.hash]
|
||||
|
||||
assert not queue_ref.is_empty
|
||||
|
||||
channel_ref.channel.receive = AsyncMock()
|
||||
|
||||
queue_ref.queue.clear()
|
||||
|
||||
await broadcast_queue.receive(channel_ref, queue_ref)
|
||||
|
||||
channel_ref.channel.receive.assert_not_awaited()
|
||||
|
||||
assert queue_ref.is_empty
|
||||
|
||||
|
||||
# endregion
|
||||
Reference in New Issue
Block a user