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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -0,0 +1,125 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import sys
from collections.abc import AsyncIterable, Awaitable, Callable
from semantic_kernel.agents.agent import Agent, AgentResponseItem, AgentThread
from semantic_kernel.agents.runtime.core.core_runtime import CoreRuntime
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
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
class MockAgentThread(AgentThread):
"""A mock agent thread for testing purposes."""
@override
async def _create(self) -> str:
return "mock_thread_id"
@override
async def _delete(self) -> None:
pass
@override
async def _on_new_message(self, new_message: ChatMessageContent) -> None:
pass
class MockAgent(Agent):
"""A mock agent for testing purposes."""
@override
async def get_response(
self,
messages: str | ChatMessageContent | list[str | ChatMessageContent] | None = None,
*,
thread: AgentThread | None = None,
**kwargs,
) -> AgentResponseItem[ChatMessageContent]:
pass
@override
async def invoke(
self,
messages: str | ChatMessageContent | list[str | ChatMessageContent] | None = None,
*,
thread: AgentThread | None = None,
on_intermediate_message: Callable[[ChatMessageContent], Awaitable[None]] | None = None,
**kwargs,
) -> AgentResponseItem[ChatMessageContent]:
pass
@override
async def invoke_stream(
self,
messages: str | ChatMessageContent | list[str | ChatMessageContent] | None = None,
*,
thread: AgentThread | None = None,
on_intermediate_message: Callable[[ChatMessageContent], Awaitable[None]] | None = None,
**kwargs,
) -> AsyncIterable[AgentResponseItem[StreamingChatMessageContent]]:
"""Simulate streaming response from the agent."""
# Simulate some processing time
await asyncio.sleep(0.05)
yield AgentResponseItem[StreamingChatMessageContent](
message=StreamingChatMessageContent(
role=AuthorRole.ASSISTANT,
name=self.name,
content="mock",
choice_index=0,
),
thread=thread or MockAgentThread(),
)
# Simulate some processing time before sending the next part of the response
await asyncio.sleep(0.05)
yield AgentResponseItem[StreamingChatMessageContent](
message=StreamingChatMessageContent(
role=AuthorRole.ASSISTANT,
name=self.name,
content="_response",
choice_index=0,
),
thread=thread or MockAgentThread(),
)
class MockAgentWithException(MockAgent):
"""A mock agent that raises an exception for testing purposes."""
@override
async def invoke_stream(
self,
messages: str | ChatMessageContent | list[str | ChatMessageContent] | None = None,
*,
thread: AgentThread | None = None,
on_intermediate_message: Callable[[ChatMessageContent], Awaitable[None]] | None = None,
**kwargs,
) -> AsyncIterable[AgentResponseItem[StreamingChatMessageContent]]:
"""Simulate streaming response from the agent that raises an exception."""
# Simulate some processing time
await asyncio.sleep(0.05)
yield AgentResponseItem[StreamingChatMessageContent](
message=StreamingChatMessageContent(
role=AuthorRole.ASSISTANT,
name=self.name,
content="mock",
choice_index=0,
),
thread=thread or MockAgentThread(),
)
raise RuntimeError("Mock agent exception")
class MockRuntime(CoreRuntime):
"""A mock agent runtime for testing purposes."""
pass
@@ -0,0 +1,204 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import sys
from unittest.mock import patch
import pytest
from semantic_kernel.agents.orchestration.concurrent import ConcurrentOrchestration
from semantic_kernel.agents.orchestration.orchestration_base import DefaultTypeAlias, OrchestrationResult
from semantic_kernel.agents.runtime.in_process.in_process_runtime import InProcessRuntime
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from tests.unit.agents.orchestration.conftest import MockAgent, MockAgentWithException, MockRuntime
async def test_prepare():
"""Test the prepare method of the ConcurrentOrchestration."""
agent_a = MockAgent()
agent_b = MockAgent()
runtime = MockRuntime()
package_path = "semantic_kernel.agents.orchestration.concurrent"
with (
patch(f"{package_path}.ConcurrentOrchestration._start"),
patch(f"{package_path}.ConcurrentAgentActor.register") as mock_agent_actor_register,
patch(f"{package_path}.CollectionActor.register") as mock_collection_actor_register,
patch.object(runtime, "add_subscription") as mock_add_subscription,
):
orchestration = ConcurrentOrchestration(members=[agent_a, agent_b])
await orchestration.invoke(task="test_message", runtime=runtime)
assert mock_agent_actor_register.call_count == 2
assert mock_collection_actor_register.call_count == 1
assert mock_add_subscription.call_count == 2
async def test_invoke():
"""Test the invoke method of the ConcurrentOrchestration."""
agent_a = MockAgent()
agent_b = MockAgent()
runtime = InProcessRuntime()
runtime.start()
try:
orchestration = ConcurrentOrchestration(members=[agent_a, agent_b])
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
result = await orchestration_result.get(1.0)
assert isinstance(orchestration_result, OrchestrationResult)
assert isinstance(result, list)
assert len(result) == 2
assert all(isinstance(item, ChatMessageContent) for item in result)
finally:
await runtime.stop_when_idle()
async def test_invoke_with_response_callback():
"""Test the invoke method of the ConcurrentOrchestration with a response callback."""
agent_a = MockAgent()
agent_b = MockAgent()
runtime = InProcessRuntime()
runtime.start()
responses: list[DefaultTypeAlias] = []
try:
orchestration = ConcurrentOrchestration(
members=[agent_a, agent_b],
agent_response_callback=lambda x: responses.append(x),
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
await orchestration_result.get(1.0)
assert len(responses) == 2
assert all(isinstance(item, ChatMessageContent) for item in responses)
assert all(item.content == "mock_response" for item in responses)
finally:
await runtime.stop_when_idle()
async def test_invoke_with_streaming_response_callback():
"""Test the invoke method of the ConcurrentOrchestration with a streaming response callback."""
agent_a = MockAgent()
agent_b = MockAgent()
runtime = InProcessRuntime()
runtime.start()
responses: dict[str, list[StreamingChatMessageContent]] = {}
try:
orchestration = ConcurrentOrchestration(
members=[agent_a, agent_b],
streaming_agent_response_callback=lambda x, _: responses.setdefault(x.name, []).append(x),
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
await orchestration_result.get(1.0)
assert len(responses[agent_a.name]) == 2
assert len(responses[agent_b.name]) == 2
assert all(isinstance(item, StreamingChatMessageContent) for item in responses[agent_a.name])
assert all(isinstance(item, StreamingChatMessageContent) for item in responses[agent_b.name])
agent_a_response = sum(responses[agent_a.name][1:], responses[agent_a.name][0])
assert agent_a_response.content == "mock_response"
agent_b_response = sum(responses[agent_b.name][1:], responses[agent_b.name][0])
assert agent_b_response.content == "mock_response"
finally:
await runtime.stop_when_idle()
@pytest.mark.skipif(
sys.version_info < (3, 11),
reason="Python 3.10 doesn't bound the original function provided to the wraps argument of the patch object.",
)
async def test_invoke_cancel_before_completion():
"""Test the invoke method of the ConcurrentOrchestration with cancellation before completion."""
with (
patch.object(MockAgent, "invoke_stream", wraps=MockAgent.invoke_stream, autospec=True) as mock_invoke_stream,
):
agent_a = MockAgent()
agent_b = MockAgent()
runtime = InProcessRuntime()
runtime.start()
try:
orchestration = ConcurrentOrchestration(members=[agent_a, agent_b])
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
# Cancel before the collection agent gets the responses
await asyncio.sleep(0.05)
orchestration_result.cancel()
finally:
await runtime.stop_when_idle()
assert mock_invoke_stream.call_count == 2
async def test_invoke_cancel_after_completion():
"""Test the invoke method of the ConcurrentOrchestration with cancellation after completion."""
agent_a = MockAgent()
agent_b = MockAgent()
runtime = InProcessRuntime()
runtime.start()
try:
orchestration = ConcurrentOrchestration(members=[agent_a, agent_b])
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
# Wait for the orchestration to complete
await orchestration_result.get(1.0)
with pytest.raises(RuntimeError, match="The invocation has already been completed."):
orchestration_result.cancel()
finally:
await runtime.stop_when_idle()
async def test_invoke_with_double_get_result():
"""Test the invoke method of the ConcurrentOrchestration with double get result."""
agent_a = MockAgent()
agent_b = MockAgent()
runtime = InProcessRuntime()
runtime.start()
try:
orchestration = ConcurrentOrchestration(members=[agent_a, agent_b])
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
# Get result before completion
with pytest.raises(asyncio.TimeoutError):
await orchestration_result.get(0.1)
# The invocation should still be in progress and getting the result again should not raise an error
result = await orchestration_result.get(1.0)
assert isinstance(result, list)
assert len(result) == 2
finally:
await runtime.stop_when_idle()
async def test_invoke_with_agent_raising_exception():
"""Test the invoke method of the ConcurrentOrchestration with an agent raising an exception."""
agent_a = MockAgent()
agent_b = MockAgentWithException()
runtime = InProcessRuntime()
runtime.start()
try:
orchestration = ConcurrentOrchestration(members=[agent_a, agent_b])
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
with pytest.raises(RuntimeError, match="Mock agent exception"):
await orchestration_result.get(1.0)
assert orchestration_result.exception is not None
finally:
await runtime.stop_when_idle()
@@ -0,0 +1,399 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import sys
from unittest.mock import patch
import pytest
from semantic_kernel.agents.orchestration.group_chat import (
BooleanResult,
GroupChatOrchestration,
RoundRobinGroupChatManager,
)
from semantic_kernel.agents.orchestration.orchestration_base import DefaultTypeAlias, OrchestrationResult
from semantic_kernel.agents.runtime.in_process.in_process_runtime import InProcessRuntime
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from tests.unit.agents.orchestration.conftest import MockAgent, MockAgentWithException, MockRuntime
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
class RoundRobinGroupChatManagerWithUserInput(RoundRobinGroupChatManager):
@override
async def should_request_user_input(self, chat_history: ChatHistory) -> BooleanResult:
"""Check if the group chat should request user input."""
return BooleanResult(
result=True,
reason="Allow user input for testing purposes.",
)
# region GroupChatOrchestration
async def test_init_member_without_description_throws():
"""Test the prepare method of the GroupChatOrchestration with a member without description."""
agent_a = MockAgent()
agent_b = MockAgent()
with pytest.raises(ValueError):
GroupChatOrchestration(members=[agent_a, agent_b], manager=RoundRobinGroupChatManager())
async def test_prepare():
"""Test the prepare method of the GroupChatOrchestration."""
agent_a = MockAgent(description="test agent")
agent_b = MockAgent(description="test agent")
runtime = MockRuntime()
package_path = "semantic_kernel.agents.orchestration.group_chat"
with (
patch(f"{package_path}.GroupChatOrchestration._start"),
patch(f"{package_path}.GroupChatAgentActor.register") as mock_agent_actor_register,
patch(f"{package_path}.GroupChatManagerActor.register") as mock_manager_actor_register,
patch.object(runtime, "add_subscription") as mock_add_subscription,
):
orchestration = GroupChatOrchestration(members=[agent_a, agent_b], manager=RoundRobinGroupChatManager())
await orchestration.invoke(task="test_message", runtime=runtime)
assert mock_agent_actor_register.call_count == 2
assert mock_manager_actor_register.call_count == 1
assert mock_add_subscription.call_count == 3
@pytest.mark.skipif(
sys.version_info < (3, 11),
reason="Python 3.10 doesn't bound the original function provided to the wraps argument of the patch object.",
)
async def test_invoke():
"""Test the invoke method of the GroupChatOrchestration."""
with (
patch.object(MockAgent, "invoke_stream", wraps=MockAgent.invoke_stream, autospec=True) as mock_invoke_stream,
):
agent_a = MockAgent(description="test agent")
agent_b = MockAgent(description="test agent")
runtime = InProcessRuntime()
runtime.start()
try:
orchestration = GroupChatOrchestration(
members=[agent_a, agent_b],
manager=RoundRobinGroupChatManager(max_rounds=3),
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
result = await orchestration_result.get(1.0)
finally:
await runtime.stop_when_idle()
assert isinstance(orchestration_result, OrchestrationResult)
assert isinstance(result, ChatMessageContent)
assert result.role == AuthorRole.ASSISTANT
assert result.content == "mock_response"
assert mock_invoke_stream.call_count == 3
@pytest.mark.skipif(
sys.version_info < (3, 11),
reason="Python 3.10 doesn't bound the original function provided to the wraps argument of the patch object.",
)
async def test_invoke_with_list():
"""Test the invoke method of the GroupChatOrchestration with a list of messages."""
with (
patch.object(MockAgent, "invoke_stream", wraps=MockAgent.invoke_stream, autospec=True) as mock_invoke_stream,
):
agent_a = MockAgent(description="test agent")
agent_b = MockAgent(description="test agent")
runtime = InProcessRuntime()
runtime.start()
messages = [
ChatMessageContent(role=AuthorRole.USER, content="test_message_1"),
ChatMessageContent(role=AuthorRole.USER, content="test_message_2"),
]
try:
orchestration = GroupChatOrchestration(
members=[agent_a, agent_b],
manager=RoundRobinGroupChatManager(max_rounds=2),
)
orchestration_result = await orchestration.invoke(task=messages, runtime=runtime)
await orchestration_result.get(1.0)
finally:
await runtime.stop_when_idle()
assert mock_invoke_stream.call_count == 2
# Two messages
assert len(mock_invoke_stream.call_args_list[0][0][1]) == 2
# Two messages + response from agent A
assert len(mock_invoke_stream.call_args_list[1][0][1]) == 3
async def test_invoke_with_response_callback():
"""Test the invoke method of the GroupChatOrchestration with a response callback."""
agent_a = MockAgent(description="test agent")
agent_b = MockAgent(description="test agent")
runtime = InProcessRuntime()
runtime.start()
responses: list[DefaultTypeAlias] = []
try:
orchestration = GroupChatOrchestration(
members=[agent_a, agent_b],
manager=RoundRobinGroupChatManager(max_rounds=3),
agent_response_callback=lambda x: responses.append(x),
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
await orchestration_result.get(1.0)
finally:
await runtime.stop_when_idle()
assert len(responses) == 3
assert all(isinstance(item, ChatMessageContent) for item in responses)
assert all(item.content == "mock_response" for item in responses)
async def test_invoke_with_streaming_response_callback():
"""Test the invoke method of the GroupChatOrchestration with a streaming_response callback."""
agent_a = MockAgent(description="test agent")
agent_b = MockAgent(description="test agent")
runtime = InProcessRuntime()
runtime.start()
responses: dict[str, list[StreamingChatMessageContent]] = {}
try:
orchestration = GroupChatOrchestration(
members=[agent_a, agent_b],
manager=RoundRobinGroupChatManager(max_rounds=3),
streaming_agent_response_callback=lambda x, _: responses.setdefault(x.name, []).append(x),
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
await orchestration_result.get(1.0)
finally:
await runtime.stop_when_idle()
assert len(responses[agent_a.name]) == 4 # Invoke twice, each with 2 chunks
assert len(responses[agent_b.name]) == 2 # Invoke once, with 2 chunks
assert all(isinstance(item, StreamingChatMessageContent) for item in responses[agent_a.name])
assert all(isinstance(item, StreamingChatMessageContent) for item in responses[agent_b.name])
agent_a_response = sum(responses[agent_a.name][1:2], responses[agent_a.name][0])
assert agent_a_response.content == "mock_response"
agent_b_response = sum(responses[agent_b.name][1:], responses[agent_b.name][0])
assert agent_b_response.content == "mock_response"
async def test_invoke_with_human_response_function():
"""Test the invoke method of the GroupChatOrchestration with a human response function."""
agent_a = MockAgent(description="test agent")
agent_b = MockAgent(description="test agent")
user_input_count = 0
def human_response_function(chat_history: ChatHistory) -> ChatMessageContent:
# Simulate user input
nonlocal user_input_count
user_input_count += 1
return ChatMessageContent(
role=AuthorRole.USER,
content=f"user_input_{user_input_count}",
)
orchestration_manager = RoundRobinGroupChatManagerWithUserInput(
max_rounds=3,
human_response_function=human_response_function,
)
runtime = InProcessRuntime()
runtime.start()
try:
orchestration = GroupChatOrchestration(
members=[agent_a, agent_b],
manager=orchestration_manager,
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
await orchestration_result.get(1.0)
finally:
await runtime.stop_when_idle()
assert user_input_count == 4 # 3 rounds + 1 initial user input
@pytest.mark.skip(
reason="Unreliable test due to timing issues in CI environment. To be fixed later.",
)
async def test_invoke_cancel_before_completion():
"""Test the invoke method of the GroupChatOrchestration with cancellation before completion."""
with (
patch.object(MockAgent, "invoke_stream", wraps=MockAgent.invoke_stream, autospec=True) as mock_invoke_stream,
):
agent_a = MockAgent(description="test agent")
agent_b = MockAgent(description="test agent")
runtime = InProcessRuntime()
runtime.start()
try:
orchestration = GroupChatOrchestration(
members=[agent_a, agent_b],
manager=RoundRobinGroupChatManager(max_rounds=3),
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
# Cancel before the second agent responds
await asyncio.sleep(0.19)
orchestration_result.cancel()
finally:
await runtime.stop_when_idle()
assert mock_invoke_stream.call_count == 2
async def test_invoke_cancel_after_completion():
"""Test the invoke method of the GroupChatOrchestration with cancellation after completion."""
agent_a = MockAgent(description="test agent")
agent_b = MockAgent(description="test agent")
runtime = InProcessRuntime()
runtime.start()
try:
orchestration = GroupChatOrchestration(
members=[agent_a, agent_b],
manager=RoundRobinGroupChatManager(max_rounds=3),
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
# Wait for the orchestration to complete
await orchestration_result.get(1.0)
with pytest.raises(RuntimeError, match="The invocation has already been completed."):
orchestration_result.cancel()
finally:
await runtime.stop_when_idle()
async def test_invoke_with_agent_raising_exception():
"""Test the invoke method of the GroupChatOrchestration with an agent raising an exception."""
agent_a = MockAgent(description="test agent")
agent_b = MockAgentWithException(description="test agent")
runtime = InProcessRuntime()
runtime.start()
try:
orchestration = GroupChatOrchestration(
members=[agent_a, agent_b],
manager=RoundRobinGroupChatManager(max_rounds=3),
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
with pytest.raises(RuntimeError, match="Mock agent exception"):
await orchestration_result.get(1.0)
assert orchestration_result.exception is not None
finally:
await runtime.stop_when_idle()
# endregion GroupChatOrchestration
# region RoundRobinGroupChatManager
def test_round_robin_group_chat_manager_init():
"""Test the initialization of the RoundRobinGroupChatManager."""
manager = RoundRobinGroupChatManager()
assert manager.max_rounds is None
assert manager.current_round == 0
assert manager.current_index == 0
assert manager.human_response_function is None
def test_round_robin_group_chat_manager_init_with_max_rounds():
"""Test the initialization of the RoundRobinGroupChatManager with max_rounds."""
manager = RoundRobinGroupChatManager(max_rounds=5)
assert manager.max_rounds == 5
assert manager.current_round == 0
assert manager.current_index == 0
assert manager.human_response_function is None
def test_round_robin_group_chat_manager_init_with_human_response_function():
"""Test the initialization of the RoundRobinGroupChatManager with human_response_function."""
async def human_response_function(chat_history: ChatHistory) -> str:
# Simulate user input
await asyncio.sleep(0.1)
return "user_input"
manager = RoundRobinGroupChatManager(human_response_function=human_response_function)
assert manager.max_rounds is None
assert manager.current_round == 0
assert manager.current_index == 0
assert manager.human_response_function == human_response_function
async def test_round_robin_group_chat_manager_should_terminate():
"""Test the should_terminate method of the RoundRobinGroupChatManager."""
manager = RoundRobinGroupChatManager(max_rounds=3)
result = await manager.should_terminate(ChatHistory())
assert result.result is False
result = await manager.should_terminate(ChatHistory())
assert result.result is False
result = await manager.should_terminate(ChatHistory())
assert result.result is False
result = await manager.should_terminate(ChatHistory())
assert result.result is True
async def test_round_robin_group_chat_manager_should_terminate_without_max_rounds():
"""Test the should_terminate method of the RoundRobinGroupChatManager without max_rounds."""
manager = RoundRobinGroupChatManager()
result = await manager.should_terminate(ChatHistory())
assert result.result is False
async def test_round_robin_group_chat_manager_select_next_agent():
"""Test the select_next_agent method of the RoundRobinGroupChatManager."""
manager = RoundRobinGroupChatManager(max_rounds=3)
participant_descriptions = {
"agent_1": "Agent 1",
"agent_2": "Agent 2",
"agent_3": "Agent 3",
}
await manager.should_terminate(ChatHistory())
result = await manager.select_next_agent(ChatHistory(), participant_descriptions)
assert result.result == "agent_1"
await manager.should_terminate(ChatHistory())
result = await manager.select_next_agent(ChatHistory(), participant_descriptions)
assert result.result == "agent_2"
await manager.should_terminate(ChatHistory())
result = await manager.select_next_agent(ChatHistory(), participant_descriptions)
assert result.result == "agent_3"
assert manager.current_round == 3
# endregion RoundRobinGroupChatManager
@@ -0,0 +1,721 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import sys
from collections.abc import AsyncIterable, Awaitable, Callable
from unittest.mock import patch
import pytest
from semantic_kernel.agents.agent import Agent, AgentResponseItem, AgentThread
from semantic_kernel.agents.orchestration.handoffs import (
HANDOFF_PLUGIN_NAME,
HandoffAgentActor,
HandoffOrchestration,
OrchestrationHandoffs,
)
from semantic_kernel.agents.orchestration.orchestration_base import DefaultTypeAlias
from semantic_kernel.agents.runtime.in_process.in_process_runtime import InProcessRuntime
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.function_call_content import FunctionCallContent
from semantic_kernel.contents.function_result_content import FunctionResultContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.kernel import Kernel
from tests.unit.agents.orchestration.conftest import MockAgent, MockAgentWithException, MockRuntime
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
class MockAgentWithHandoffFunctionCall(Agent):
"""A mock agent with handoff function call for testing purposes."""
target_agent: Agent
def __init__(self, target_agent: Agent):
super().__init__(target_agent=target_agent)
@override
async def get_response(
self,
messages: str | ChatMessageContent | list[str | ChatMessageContent] | None = None,
*,
thread: AgentThread | None = None,
kernel: Kernel | None = None,
**kwargs,
) -> AgentResponseItem[ChatMessageContent]:
pass
@override
async def invoke(
self,
messages: str | ChatMessageContent | list[str | ChatMessageContent] | None = None,
*,
thread: AgentThread | None = None,
on_intermediate_message: Callable[[ChatMessageContent], Awaitable[None]] | None = None,
**kwargs,
) -> AgentResponseItem[ChatMessageContent]:
pass
@override
async def invoke_stream(
self,
messages: str | ChatMessageContent | list[str | ChatMessageContent] | None = None,
*,
thread: AgentThread | None = None,
on_intermediate_message: Callable[[ChatMessageContent], Awaitable[None]] | None = None,
kernel: Kernel | None = None,
**kwargs,
) -> AsyncIterable[AgentResponseItem[StreamingChatMessageContent]]:
"""Simulate streaming response from the agent."""
function_call = FunctionCallContent(
function_name=f"transfer_to_{self.target_agent.name}",
plugin_name=HANDOFF_PLUGIN_NAME,
call_id="test_call_id",
id="test_id",
)
# Simulate some processing time
await asyncio.sleep(0.1)
await kernel.invoke_function_call(
function_call=function_call,
chat_history=ChatHistory(),
)
# Do not yield any messages, as the agent doesn't yield any tool related messages from the streaming API.
# Nevertheless, the method needs have a `yield` code path to satisfy the AsyncIterable interface.
if False:
yield
# Simulate on_intermediate_message callback
await on_intermediate_message(
StreamingChatMessageContent(
role=AuthorRole.ASSISTANT,
name=self.name,
choice_index=0,
items=[function_call],
)
)
await on_intermediate_message(
StreamingChatMessageContent(
role=AuthorRole.ASSISTANT,
name=self.name,
choice_index=0,
items=[
FunctionResultContent(
function_name=function_call.function_name,
plugin_name=function_call.plugin_name,
call_id=function_call.call_id,
id=function_call.id,
result=None,
)
],
)
)
class MockAgentWithCompleteTaskFunctionCall(Agent):
"""A mock agent with complete_task function call for testing purposes."""
@override
async def get_response(
self,
messages: str | ChatMessageContent | list[str | ChatMessageContent] | None = None,
*,
thread: AgentThread | None = None,
kernel: Kernel | None = None,
**kwargs,
) -> AgentResponseItem[ChatMessageContent]:
pass
@override
async def invoke(
self,
messages: str | ChatMessageContent | list[str | ChatMessageContent] | None = None,
*,
thread: AgentThread | None = None,
on_intermediate_message: Callable[[ChatMessageContent], Awaitable[None]] | None = None,
**kwargs,
) -> AgentResponseItem[ChatMessageContent]:
pass
@override
async def invoke_stream(
self,
messages: str | ChatMessageContent | list[str | ChatMessageContent] | None = None,
*,
thread: AgentThread | None = None,
on_intermediate_message: Callable[[ChatMessageContent], Awaitable[None]] | None = None,
kernel: Kernel | None = None,
**kwargs,
) -> AsyncIterable[AgentResponseItem[StreamingChatMessageContent]]:
"""Simulate streaming response from the agent."""
# Simulate some processing time
await asyncio.sleep(0.1)
await kernel.invoke_function_call(
function_call=FunctionCallContent(
function_name="complete_task",
plugin_name=HANDOFF_PLUGIN_NAME,
call_id="test_call_id",
id="test_id",
arguments={"task_summary": "test_summary"},
),
chat_history=ChatHistory(),
)
# Do not yield any messages, as the agent doesn't yield any tool related messages from the streaming API.
# Nevertheless, the method needs have a `yield` code path to satisfy the AsyncIterable interface.
if False:
yield
# region HandoffOrchestration
def test_init_without_handoffs():
"""Test the initialization of HandoffOrchestration without handoffs."""
agent_a = MockAgent()
agent_b = MockAgent()
with pytest.raises(ValueError):
HandoffOrchestration(members=[agent_a, agent_b], handoffs={})
def test_init_with_invalid_handoff():
"""Test the initialization of HandoffOrchestration with invalid handoff."""
agent_a = MockAgent()
agent_b = MockAgent()
# Invalid handoff agent name
with pytest.raises(ValueError):
HandoffOrchestration(
members=[agent_a, agent_b],
handoffs={
agent_a.name: {agent_b.name: "test", "invalid_agent_name": "test"},
agent_b.name: {agent_a.name: "test"},
},
)
# Invalid handoff agent name (not in members)
with pytest.raises(ValueError):
HandoffOrchestration(
members=[agent_a, agent_b],
handoffs={
"invalid_agent_name": {agent_b.name: "test"},
agent_b.name: {agent_a.name: "test"},
},
)
# Cannot handoff to self
with pytest.raises(ValueError):
HandoffOrchestration(
members=[agent_a, agent_b],
handoffs={
agent_a.name: {agent_a.name: "test"},
agent_b.name: {agent_a.name: "test"},
},
)
def test_init_with_duplicate_handoffs():
"""Test the initialization of HandoffOrchestration with duplicate handoffs."""
agent_a = MockAgent()
agent_b = MockAgent()
# Uniqueness guarantee
orchestration = HandoffOrchestration(
members=[agent_a, agent_b],
handoffs={
agent_a.name: {agent_b.name: "test 1", agent_b.name: "test 2"},
},
)
assert len(orchestration._handoffs[agent_a.name]) == 1
def test_init_with_dictionary_handoffs():
"""Test the initialization of HandoffOrchestration with dictionary handoffs."""
agent_a = MockAgent()
agent_b = MockAgent()
orchestration_handoffs = OrchestrationHandoffs(
{
agent_a.name: {agent_b.name: "test 1"},
agent_b.name: {agent_a.name: "test 2"},
},
)
assert len(orchestration_handoffs) == 2
for handoff_agent_name, handoff_description in orchestration_handoffs[agent_a.name].items():
assert handoff_agent_name == agent_b.name
assert handoff_description == "test 1"
for handoff_agent_name, handoff_description in orchestration_handoffs[agent_b.name].items():
assert handoff_agent_name == agent_a.name
assert handoff_description == "test 2"
def test_orchestration_handoff_add():
"""Test the add method of the OrchestrationHandoffs."""
agent_a = MockAgent()
agent_b = MockAgent()
orchestration_handoffs = OrchestrationHandoffs().add(agent_a, agent_b).add(agent_b, agent_a)
assert isinstance(orchestration_handoffs, OrchestrationHandoffs)
assert len(orchestration_handoffs) == 2
assert len(orchestration_handoffs[agent_a.name]) == 1
assert len(orchestration_handoffs[agent_b.name]) == 1
for handoff_agent_name, handoff_description in orchestration_handoffs[agent_a.name].items():
assert handoff_agent_name == agent_b.name
assert handoff_description == ""
for handoff_agent_name, handoff_description in orchestration_handoffs[agent_b.name].items():
assert handoff_agent_name == agent_a.name
assert handoff_description == ""
def test_orchestration_handoff_add_many():
"""Test the add_many method of the OrchestrationHandoffs."""
agent_a = MockAgent(description="agent_a")
agent_b = MockAgent(description="agent_b")
agent_c = MockAgent(description="agent_c")
# Case 1: Agent instance as source and dictionary as handoffs
orchestration_handoffs = OrchestrationHandoffs().add_many(
agent_a,
{agent_b.name: "test 1", agent_c.name: "test 2"},
)
assert isinstance(orchestration_handoffs, OrchestrationHandoffs)
assert len(orchestration_handoffs) == 1
assert len(orchestration_handoffs[agent_a.name]) == 2
for handoff_agent_name, handoff_description in orchestration_handoffs[agent_a.name].items():
assert handoff_agent_name in [agent_b.name, agent_c.name]
assert handoff_description in ["test 1", "test 2"]
# Case 2: Agent name as source and list of agents as handoffs
orchestration_handoffs = OrchestrationHandoffs().add_many(
agent_a.name,
{agent_b.name: "test 1", agent_c.name: "test 2"},
)
assert isinstance(orchestration_handoffs, OrchestrationHandoffs)
assert len(orchestration_handoffs) == 1
assert len(orchestration_handoffs[agent_a.name]) == 2
for handoff_agent_name, handoff_description in orchestration_handoffs[agent_a.name].items():
assert handoff_agent_name in [agent_b.name, agent_c.name]
assert handoff_description in ["test 1", "test 2"]
# Case 3: Agent instance as source and list of agents as handoffs
orchestration_handoffs = OrchestrationHandoffs().add_many(agent_a, [agent_b, agent_c])
assert isinstance(orchestration_handoffs, OrchestrationHandoffs)
assert len(orchestration_handoffs) == 1
assert len(orchestration_handoffs[agent_a.name]) == 2
for handoff_agent_name, handoff_description in orchestration_handoffs[agent_a.name].items():
assert handoff_agent_name in [agent_b.name, agent_c.name]
assert handoff_description in [agent_b.description, agent_c.description]
# Case 4: Agent name as source and list of agent names as handoffs
orchestration_handoffs = OrchestrationHandoffs().add_many(agent_a.name, [agent_b.name, agent_c.name])
assert isinstance(orchestration_handoffs, OrchestrationHandoffs)
assert len(orchestration_handoffs) == 1
assert len(orchestration_handoffs[agent_a.name]) == 2
for handoff_agent_name, handoff_description in orchestration_handoffs[agent_a.name].items():
assert handoff_agent_name in [agent_b.name, agent_c.name]
assert handoff_description == ""
async def test_prepare():
"""Test the prepare method of the HandoffOrchestration."""
agent_a = MockAgent()
agent_b = MockAgent()
agent_c = MockAgent()
runtime = MockRuntime()
package_path = "semantic_kernel.agents.orchestration.handoffs"
with (
patch(f"{package_path}.HandoffOrchestration._start"),
patch(f"{package_path}.HandoffAgentActor.register") as mock_agent_actor_register,
patch.object(runtime, "add_subscription") as mock_add_subscription,
):
orchestration = HandoffOrchestration(
members=[agent_a, agent_b, agent_c],
handoffs={
agent_a.name: {agent_b.name: "test"},
agent_b.name: {agent_c.name: "test"},
agent_c.name: {agent_a.name: "test"},
},
)
await orchestration.invoke(task="test_message", runtime=runtime)
assert mock_agent_actor_register.call_count == 3
assert mock_add_subscription.call_count == 3
@pytest.mark.skipif(
sys.version_info < (3, 11),
reason="Python 3.10 doesn't bound the original function provided to the wraps argument of the patch object.",
)
async def test_invoke():
"""Test the prepare method of the HandoffOrchestration."""
with (
patch.object(HandoffAgentActor, "__init__", wraps=HandoffAgentActor.__init__, autospec=True) as mock_init,
patch.object(MockAgent, "invoke_stream", wraps=MockAgent.invoke_stream, autospec=True) as mock_invoke_stream,
):
agent_a = MockAgent()
agent_b = MockAgent()
agent_c = MockAgent()
runtime = InProcessRuntime()
runtime.start()
try:
orchestration = HandoffOrchestration(
members=[agent_a, agent_b, agent_c],
handoffs={
agent_a.name: {agent_b.name: "test", agent_c.name: "test"},
agent_b.name: {agent_a.name: "test"},
},
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
await orchestration_result.get(1.0)
assert mock_init.call_args_list[0][0][3] == {agent_b.name: "test", agent_c.name: "test"}
assert isinstance(mock_invoke_stream.call_args_list[0][1]["kernel"], Kernel)
kernel = mock_invoke_stream.call_args_list[0][1]["kernel"]
assert HANDOFF_PLUGIN_NAME in kernel.plugins
assert (
len(kernel.plugins[HANDOFF_PLUGIN_NAME].functions) == 3
) # two handoff functions + complete task function
# The kernel in the agent should not be modified
assert len(agent_a.kernel.plugins) == 0
assert len(agent_b.kernel.plugins) == 0
assert len(agent_c.kernel.plugins) == 0
finally:
await runtime.stop_when_idle()
@pytest.mark.skipif(
sys.version_info < (3, 11),
reason="Python 3.10 doesn't bound the original function provided to the wraps argument of the patch object.",
)
async def test_invoke_with_list():
"""Test the invoke method of the HandoffOrchestration with a list of messages."""
with (
patch.object(MockAgent, "invoke_stream", wraps=MockAgent.invoke_stream, autospec=True) as mock_invoke_stream,
):
agent_a = MockAgent()
agent_b = MockAgent()
runtime = InProcessRuntime()
runtime.start()
messages = [
ChatMessageContent(role=AuthorRole.USER, content="test_message_1"),
ChatMessageContent(role=AuthorRole.USER, content="test_message_2"),
]
try:
orchestration = HandoffOrchestration(
members=[agent_a, agent_b],
handoffs={agent_a.name: {agent_b.name: "test"}},
)
orchestration_result = await orchestration.invoke(task=messages, runtime=runtime)
await orchestration_result.get(1.0)
finally:
await runtime.stop_when_idle()
assert mock_invoke_stream.call_count == 1
# Two messages
assert len(mock_invoke_stream.call_args_list[0][0][1]) == 2
# The kernel in the agent should not be modified
assert len(agent_a.kernel.plugins) == 0
assert len(agent_b.kernel.plugins) == 0
async def test_invoke_with_response_callback():
"""Test the invoke method of the HandoffOrchestration with a response callback."""
agent_a = MockAgent()
agent_b = MockAgent()
runtime = InProcessRuntime()
runtime.start()
responses: list[DefaultTypeAlias] = []
try:
orchestration = HandoffOrchestration(
members=[agent_a, agent_b],
handoffs={agent_a.name: {agent_b.name: "test"}},
agent_response_callback=lambda x: responses.append(x),
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
await orchestration_result.get(1.0)
finally:
await runtime.stop_when_idle()
assert len(responses) == 1
assert all(isinstance(item, ChatMessageContent) for item in responses)
assert all(item.content == "mock_response" for item in responses)
# The kernel in the agent should not be modified
assert len(agent_a.kernel.plugins) == 0
assert len(agent_b.kernel.plugins) == 0
async def test_invoke_with_streaming_response_callback():
"""Test the invoke method of the HandoffOrchestration with a streaming response callback."""
agent_a = MockAgent()
agent_b = MockAgent()
runtime = InProcessRuntime()
runtime.start()
responses: dict[str, list[StreamingChatMessageContent]] = {}
try:
orchestration = HandoffOrchestration(
members=[agent_a, agent_b],
handoffs={agent_a.name: {agent_b.name: "test"}},
streaming_agent_response_callback=lambda x, _: responses.setdefault(x.name, []).append(x),
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
await orchestration_result.get(1.0)
finally:
await runtime.stop_when_idle()
assert len(responses[agent_a.name]) == 2
assert all(isinstance(item, StreamingChatMessageContent) for item in responses[agent_a.name])
agent_a_response = sum(responses[agent_a.name][1:], responses[agent_a.name][0])
assert agent_a_response.content == "mock_response"
# Agent B was not invoked, so it should not have any responses
assert agent_b.name not in responses or len(responses[agent_b.name]) == 0
# The kernel in the agent should not be modified
assert len(agent_a.kernel.plugins) == 0
assert len(agent_b.kernel.plugins) == 0
async def test_response_callback_with_handoff_function_call():
"""Test the response callback of the HandoffOrchestration with a handoff function call."""
agent_b = MockAgent()
agent_a = MockAgentWithHandoffFunctionCall(agent_b)
runtime = InProcessRuntime()
runtime.start()
responses: list[DefaultTypeAlias] = []
try:
orchestration = HandoffOrchestration(
members=[agent_a, agent_b],
handoffs={agent_a.name: {agent_b.name: "test"}},
agent_response_callback=lambda x: responses.append(x),
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
await orchestration_result.get(1.0)
finally:
await runtime.stop_when_idle()
assert len(responses) == 3
assert responses[0].name == agent_a.name
assert isinstance(responses[0].items[0], FunctionCallContent)
assert responses[1].name == agent_a.name
assert isinstance(responses[1].items[0], FunctionResultContent)
assert responses[2].name == agent_b.name
async def test_streaming_response_callback_with_handoff_function_call():
"""Test the streaming sresponse callback of the HandoffOrchestration with a handoff function call."""
agent_b = MockAgent()
agent_a = MockAgentWithHandoffFunctionCall(agent_b)
runtime = InProcessRuntime()
runtime.start()
responses: dict[str, list[StreamingChatMessageContent]] = {}
try:
orchestration = HandoffOrchestration(
members=[agent_a, agent_b],
handoffs={agent_a.name: {agent_b.name: "test"}},
streaming_agent_response_callback=lambda x, _: responses.setdefault(x.name, []).append(x),
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
await orchestration_result.get(1.0)
finally:
await runtime.stop_when_idle()
assert len(responses[agent_a.name]) == 2
assert len(responses[agent_b.name]) == 2 # 2 chunks
assert responses[agent_a.name][0].name == agent_a.name
assert isinstance(responses[agent_a.name][0].items[0], FunctionCallContent)
assert responses[agent_a.name][1].name == agent_a.name
assert isinstance(responses[agent_a.name][1].items[0], FunctionResultContent)
assert responses[agent_b.name][0].name == agent_b.name
assert all([isinstance(response, StreamingChatMessageContent) for response in responses[agent_b.name]])
@pytest.mark.skipif(
sys.version_info < (3, 11),
reason="Python 3.10 doesn't bound the original function provided to the wraps argument of the patch object.",
)
async def test_invoke_with_human_response_function():
"""Test the invoke method of the HandoffOrchestration with a human response function."""
complete_task_agent_instance = MockAgentWithCompleteTaskFunctionCall()
normal_agent_instance = MockAgent()
call_sequence = iter([normal_agent_instance.invoke_stream, complete_task_agent_instance.invoke_stream])
user_input_count = 0
def human_response_function() -> ChatMessageContent:
# Simulate user input
nonlocal user_input_count
user_input_count += 1
return ChatMessageContent(role=AuthorRole.USER, content="user_input")
with (
patch.object(MockAgent, "invoke_stream") as mock_invoke_stream,
):
mock_invoke_stream.side_effect = lambda *args, **kwargs: next(call_sequence)(*args, **kwargs)
agent_a = MockAgent(name="agent_a")
agent_b = MockAgent(name="agent_b")
runtime = InProcessRuntime()
runtime.start()
try:
orchestration = HandoffOrchestration(
members=[agent_a, agent_b],
handoffs={agent_a.name: {agent_b.name: "test"}},
human_response_function=human_response_function,
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
await orchestration_result.get(1.0)
finally:
await runtime.stop_when_idle()
assert user_input_count == 1
@pytest.mark.skipif(
sys.version_info < (3, 11),
reason="Python 3.10 doesn't bound the original function provided to the wraps argument of the patch object.",
)
async def test_invoke_with_handoff_function_call():
"""Test the invoke method of the HandoffOrchestration with a handoff function call."""
agent_b = MockAgent()
agent_a = MockAgentWithHandoffFunctionCall(agent_b)
with (
patch.object(
HandoffAgentActor, "_handoff_to_agent", wraps=HandoffAgentActor._handoff_to_agent, autospec=True
) as mock_handoff_to_agent,
):
runtime = InProcessRuntime()
runtime.start()
try:
orchestration = HandoffOrchestration(
members=[agent_a, agent_b],
handoffs={agent_a.name: {agent_b.name: "test"}},
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
await orchestration_result.get(1.0)
finally:
await runtime.stop_when_idle()
assert mock_handoff_to_agent.call_count == 1
assert mock_handoff_to_agent.call_args_list[0][0][1] == agent_b.name
# The kernel in the agent should not be modified
assert len(agent_a.kernel.plugins) == 0
assert len(agent_b.kernel.plugins) == 0
@pytest.mark.skipif(
sys.version_info < (3, 11),
reason="Python 3.10 doesn't bound the original function provided to the wraps argument of the patch object.",
)
async def test_invoke_cancel_before_completion():
"""Test the invoke method of the HandoffOrchestration with cancellation before completion."""
with (
patch.object(MockAgent, "invoke_stream", wraps=MockAgent.invoke_stream, autospec=True) as mock_invoke_stream,
):
agent_a = MockAgent()
agent_b = MockAgent()
runtime = InProcessRuntime()
runtime.start()
try:
orchestration = HandoffOrchestration(
members=[agent_a, agent_b],
handoffs={agent_a.name: {agent_b.name: "test"}},
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
# Cancel before first agent completes
await asyncio.sleep(0.05)
orchestration_result.cancel()
finally:
await runtime.stop_when_idle()
assert mock_invoke_stream.call_count == 1
async def test_invoke_cancel_after_completion():
"""Test the invoke method of the HandoffOrchestration with cancellation after completion."""
agent_a = MockAgent()
agent_b = MockAgent()
runtime = InProcessRuntime()
runtime.start()
try:
orchestration = HandoffOrchestration(
members=[agent_a, agent_b],
handoffs={agent_a.name: {agent_b.name: "test"}},
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
# Wait for the orchestration to complete
await orchestration_result.get(1.0)
with pytest.raises(RuntimeError, match="The invocation has already been completed."):
orchestration_result.cancel()
finally:
await runtime.stop_when_idle()
async def test_invoke_with_agent_raising_exception():
"""Test the invoke method of the HandoffOrchestration with an agent raising an exception."""
agent_a = MockAgentWithException()
agent_b = MockAgent()
runtime = InProcessRuntime()
runtime.start()
try:
orchestration = HandoffOrchestration(
members=[agent_a, agent_b],
handoffs={agent_a.name: {agent_b.name: "test"}},
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
with pytest.raises(RuntimeError, match="Mock agent exception"):
await orchestration_result.get(1.0)
assert orchestration_result.exception is not None
finally:
await runtime.stop_when_idle()
# endregion
@@ -0,0 +1,806 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from typing import Any, Literal
from unittest.mock import AsyncMock, patch
import pytest
from pydantic import BaseModel
from semantic_kernel.agents.orchestration.magentic import (
MagenticContext,
MagenticOrchestration,
ProgressLedger,
ProgressLedgerItem,
StandardMagenticManager,
)
from semantic_kernel.agents.orchestration.orchestration_base import DefaultTypeAlias, OrchestrationResult
from semantic_kernel.agents.orchestration.prompts._magentic_prompts import (
ORCHESTRATOR_FINAL_ANSWER_PROMPT,
ORCHESTRATOR_PROGRESS_LEDGER_PROMPT,
ORCHESTRATOR_TASK_LEDGER_FACTS_PROMPT,
ORCHESTRATOR_TASK_LEDGER_FACTS_UPDATE_PROMPT,
ORCHESTRATOR_TASK_LEDGER_FULL_PROMPT,
ORCHESTRATOR_TASK_LEDGER_PLAN_PROMPT,
ORCHESTRATOR_TASK_LEDGER_PLAN_UPDATE_PROMPT,
)
from semantic_kernel.agents.runtime.in_process.in_process_runtime import InProcessRuntime
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from tests.unit.agents.orchestration.conftest import MockAgent, MockAgentWithException, MockRuntime
class MockChatCompletionService(ChatCompletionClientBase):
"""A mock chat completion service for testing purposes."""
pass
class MockPromptExecutionSettings(PromptExecutionSettings):
"""A mock prompt execution settings class for testing purposes."""
response_format: (
dict[Literal["type"], Literal["text", "json_object"]] | dict[str, Any] | type[BaseModel] | type | None
) = None
# region MagenticOrchestration
async def test_init_member_without_description_throws():
"""Test the prepare method of the MagenticOrchestration with a member without description."""
agent_a = MockAgent()
agent_b = MockAgent()
with pytest.raises(ValueError):
MagenticOrchestration(
members=[agent_a, agent_b],
manager=StandardMagenticManager(
chat_completion_service=MockChatCompletionService(ai_model_id="test"),
prompt_execution_settings=MockPromptExecutionSettings(),
),
)
async def test_prepare():
"""Test the prepare method of the MagenticOrchestration."""
agent_a = MockAgent(description="test agent")
agent_b = MockAgent(description="test agent")
runtime = MockRuntime()
package_path = "semantic_kernel.agents.orchestration.magentic"
with (
patch(f"{package_path}.MagenticOrchestration._start"),
patch(f"{package_path}.MagenticAgentActor.register") as mock_agent_actor_register,
patch(f"{package_path}.MagenticManagerActor.register") as mock_manager_actor_register,
patch.object(runtime, "add_subscription") as mock_add_subscription,
):
orchestration = MagenticOrchestration(
members=[agent_a, agent_b],
manager=StandardMagenticManager(
chat_completion_service=MockChatCompletionService(ai_model_id="test"),
prompt_execution_settings=MockPromptExecutionSettings(),
),
)
await orchestration.invoke(task="test_message", runtime=runtime)
assert mock_agent_actor_register.call_count == 2
assert mock_manager_actor_register.call_count == 1
assert mock_add_subscription.call_count == 3
ManagerProgressList = [
ProgressLedger(
is_request_satisfied=ProgressLedgerItem(answer=False, reason="mock_reasoning"),
is_in_loop=ProgressLedgerItem(answer=False, reason="mock_reasoning"),
is_progress_being_made=ProgressLedgerItem(answer=True, reason="mock_reasoning"),
next_speaker=ProgressLedgerItem(answer="agent_a", reason="mock_reasoning"),
instruction_or_question=ProgressLedgerItem(answer="mock_instruction", reason="mock_reasoning"),
),
ProgressLedger(
is_request_satisfied=ProgressLedgerItem(answer=False, reason="mock_reasoning"),
is_in_loop=ProgressLedgerItem(answer=False, reason="mock_reasoning"),
is_progress_being_made=ProgressLedgerItem(answer=True, reason="mock_reasoning"),
next_speaker=ProgressLedgerItem(answer="agent_b", reason="mock_reasoning"),
instruction_or_question=ProgressLedgerItem(answer="mock_instruction", reason="mock_reasoning"),
),
ProgressLedger(
is_request_satisfied=ProgressLedgerItem(answer=True, reason="mock_reasoning"),
is_in_loop=ProgressLedgerItem(answer=False, reason="mock_reasoning"),
is_progress_being_made=ProgressLedgerItem(answer=True, reason="mock_reasoning"),
next_speaker=ProgressLedgerItem(answer="N/A", reason="mock_reasoning"),
instruction_or_question=ProgressLedgerItem(answer="mock_instruction", reason="mock_reasoning"),
),
]
ManagerProgressListStalling = [
ProgressLedger(
is_request_satisfied=ProgressLedgerItem(answer=False, reason="mock_reasoning"),
is_in_loop=ProgressLedgerItem(answer=False, reason="mock_reasoning"),
is_progress_being_made=ProgressLedgerItem(answer=True, reason="mock_reasoning"),
next_speaker=ProgressLedgerItem(answer="agent_a", reason="mock_reasoning"),
instruction_or_question=ProgressLedgerItem(answer="mock_instruction", reason="mock_reasoning"),
),
ProgressLedger(
is_request_satisfied=ProgressLedgerItem(answer=False, reason="mock_reasoning"),
is_in_loop=ProgressLedgerItem(answer=True, reason="mock_reasoning"), # is_in_loop=True
is_progress_being_made=ProgressLedgerItem(answer=True, reason="mock_reasoning"),
next_speaker=ProgressLedgerItem(answer="agent_a", reason="mock_reasoning"),
instruction_or_question=ProgressLedgerItem(answer="mock_instruction", reason="mock_reasoning"),
),
ProgressLedger(
is_request_satisfied=ProgressLedgerItem(answer=False, reason="mock_reasoning"),
is_in_loop=ProgressLedgerItem(answer=True, reason="mock_reasoning"), # is_in_loop=True
is_progress_being_made=ProgressLedgerItem(answer=True, reason="mock_reasoning"),
next_speaker=ProgressLedgerItem(answer="N/A", reason="mock_reasoning"),
instruction_or_question=ProgressLedgerItem(answer="mock_instruction", reason="mock_reasoning"),
),
ProgressLedger(
is_request_satisfied=ProgressLedgerItem(answer=False, reason="mock_reasoning"),
is_in_loop=ProgressLedgerItem(answer=False, reason="mock_reasoning"),
is_progress_being_made=ProgressLedgerItem(answer=True, reason="mock_reasoning"),
next_speaker=ProgressLedgerItem(answer="agent_b", reason="mock_reasoning"),
instruction_or_question=ProgressLedgerItem(answer="mock_instruction", reason="mock_reasoning"),
),
ProgressLedger(
is_request_satisfied=ProgressLedgerItem(answer=True, reason="mock_reasoning"),
is_in_loop=ProgressLedgerItem(answer=False, reason="mock_reasoning"),
is_progress_being_made=ProgressLedgerItem(answer=True, reason="mock_reasoning"),
next_speaker=ProgressLedgerItem(answer="N/A", reason="mock_reasoning"),
instruction_or_question=ProgressLedgerItem(answer="mock_instruction", reason="mock_reasoning"),
),
]
ManagerProgressListUnknownSpeaker = [
ProgressLedger(
is_request_satisfied=ProgressLedgerItem(answer=False, reason="mock_reasoning"),
is_in_loop=ProgressLedgerItem(answer=False, reason="mock_reasoning"),
is_progress_being_made=ProgressLedgerItem(answer=True, reason="mock_reasoning"),
next_speaker=ProgressLedgerItem(answer="unknown", reason="mock_reasoning"),
instruction_or_question=ProgressLedgerItem(answer="mock_instruction", reason="mock_reasoning"),
),
]
@pytest.mark.skipif(
sys.version_info < (3, 11),
reason="Python 3.10 doesn't bound the original function provided to the wraps argument of the patch object.",
)
async def test_invoke():
"""Test the invoke method of the MagenticOrchestration."""
with (
patch.object(MockAgent, "invoke_stream", wraps=MockAgent.invoke_stream, autospec=True) as mock_invoke_stream,
patch.object(
MockChatCompletionService, "get_chat_message_content", new_callable=AsyncMock
) as mock_get_chat_message_content,
patch.object(
StandardMagenticManager, "create_progress_ledger", new_callable=AsyncMock, side_effect=ManagerProgressList
),
):
mock_get_chat_message_content.return_value = ChatMessageContent(role="assistant", content="mock_response")
chat_completion_service = MockChatCompletionService(ai_model_id="test")
prompt_execution_settings = MockPromptExecutionSettings()
manager = StandardMagenticManager(
chat_completion_service=chat_completion_service,
prompt_execution_settings=prompt_execution_settings,
)
agent_a = MockAgent(name="agent_a", description="test agent")
agent_b = MockAgent(name="agent_b", description="test agent")
runtime = InProcessRuntime()
runtime.start()
try:
orchestration = MagenticOrchestration(members=[agent_a, agent_b], manager=manager)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
result = await orchestration_result.get(1.0)
finally:
await runtime.stop_when_idle()
assert isinstance(orchestration_result, OrchestrationResult)
assert isinstance(result, ChatMessageContent)
assert result.role == AuthorRole.ASSISTANT
assert result.content == "mock_response"
assert mock_invoke_stream.call_count == 2
assert mock_get_chat_message_content.call_count == 3
async def test_invoke_with_list_error():
"""Test the invoke method of the MagenticOrchestration with a list of messages which raises an error."""
chat_completion_service = MockChatCompletionService(ai_model_id="test")
prompt_execution_settings = MockPromptExecutionSettings()
manager = StandardMagenticManager(
chat_completion_service=chat_completion_service,
prompt_execution_settings=prompt_execution_settings,
)
agent_a = MockAgent(name="agent_a", description="test agent")
agent_b = MockAgent(name="agent_b", description="test agent")
messages = [
ChatMessageContent(role=AuthorRole.USER, content="test_message_1"),
ChatMessageContent(role=AuthorRole.USER, content="test_message_2"),
]
runtime = MockRuntime()
package_path = "semantic_kernel.agents.orchestration.magentic"
with (
patch(f"{package_path}.MagenticAgentActor.register"),
patch(f"{package_path}.MagenticManagerActor.register"),
patch.object(runtime, "add_subscription"),
pytest.raises(ValueError),
):
orchestration = MagenticOrchestration(members=[agent_a, agent_b], manager=manager)
orchestration_result = await orchestration.invoke(task=messages, runtime=runtime)
await orchestration_result.get(1.0)
async def test_invoke_with_agent_raising_exception():
"""Test the invoke method of the MagenticOrchestration with a list of messages which raises an error."""
with (
patch.object(
MockChatCompletionService, "get_chat_message_content", new_callable=AsyncMock
) as mock_get_chat_message_content,
patch.object(
StandardMagenticManager, "create_progress_ledger", new_callable=AsyncMock, side_effect=ManagerProgressList
),
):
mock_get_chat_message_content.return_value = ChatMessageContent(role="assistant", content="mock_response")
chat_completion_service = MockChatCompletionService(ai_model_id="test")
prompt_execution_settings = MockPromptExecutionSettings()
manager = StandardMagenticManager(
chat_completion_service=chat_completion_service,
prompt_execution_settings=prompt_execution_settings,
)
agent_a = MockAgentWithException(name="agent_a", description="test agent")
agent_b = MockAgent(name="agent_b", description="test agent")
runtime = InProcessRuntime()
runtime.start()
orchestration = MagenticOrchestration(members=[agent_a, agent_b], manager=manager)
try:
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
with pytest.raises(RuntimeError, match="Mock agent exception"):
await orchestration_result.get(1.0)
assert orchestration_result.exception is not None
finally:
await runtime.stop_when_idle()
@pytest.mark.skipif(
sys.version_info < (3, 11),
reason="Python 3.10 doesn't bound the original function provided to the wraps argument of the patch object.",
)
async def test_invoke_with_response_callback():
"""Test the invoke method of the MagenticOrchestration with a response callback."""
runtime = InProcessRuntime()
runtime.start()
responses: list[DefaultTypeAlias] = []
with (
patch.object(
MockChatCompletionService, "get_chat_message_content", new_callable=AsyncMock
) as mock_get_chat_message_content,
patch.object(
StandardMagenticManager, "create_progress_ledger", new_callable=AsyncMock, side_effect=ManagerProgressList
),
):
mock_get_chat_message_content.return_value = ChatMessageContent(role="assistant", content="mock_response")
agent_a = MockAgent(name="agent_a", description="test agent")
agent_b = MockAgent(name="agent_b", description="test agent")
try:
orchestration = MagenticOrchestration(
members=[agent_a, agent_b],
manager=StandardMagenticManager(
chat_completion_service=MockChatCompletionService(ai_model_id="test"),
prompt_execution_settings=MockPromptExecutionSettings(),
),
agent_response_callback=lambda x: responses.append(x),
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
await orchestration_result.get(1.0)
finally:
await runtime.stop_when_idle()
assert len(responses) == 2
assert all(isinstance(item, ChatMessageContent) for item in responses)
assert all(item.content == "mock_response" for item in responses)
@pytest.mark.skipif(
sys.version_info < (3, 11),
reason="Python 3.10 doesn't bound the original function provided to the wraps argument of the patch object.",
)
async def test_invoke_with_streaming_response_callback():
"""Test the invoke method of the MagenticOrchestration with a streaming response callback."""
runtime = InProcessRuntime()
runtime.start()
responses: dict[str, list[StreamingChatMessageContent]] = {}
with (
patch.object(
MockChatCompletionService, "get_chat_message_content", new_callable=AsyncMock
) as mock_get_chat_message_content,
patch.object(
StandardMagenticManager, "create_progress_ledger", new_callable=AsyncMock, side_effect=ManagerProgressList
),
):
mock_get_chat_message_content.return_value = ChatMessageContent(role="assistant", content="mock_response")
agent_a = MockAgent(name="agent_a", description="test agent")
agent_b = MockAgent(name="agent_b", description="test agent")
try:
orchestration = MagenticOrchestration(
members=[agent_a, agent_b],
manager=StandardMagenticManager(
chat_completion_service=MockChatCompletionService(ai_model_id="test"),
prompt_execution_settings=MockPromptExecutionSettings(),
),
streaming_agent_response_callback=lambda x, _: responses.setdefault(x.name, []).append(x),
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
await orchestration_result.get(1.0)
finally:
await runtime.stop_when_idle()
assert len(responses[agent_a.name]) == 2
assert len(responses[agent_b.name]) == 2
assert all(isinstance(item, StreamingChatMessageContent) for item in responses[agent_a.name])
assert all(isinstance(item, StreamingChatMessageContent) for item in responses[agent_b.name])
agent_a_response = sum(responses[agent_a.name][1:], responses[agent_a.name][0])
assert agent_a_response.content == "mock_response"
agent_b_response = sum(responses[agent_b.name][1:], responses[agent_b.name][0])
assert agent_b_response.content == "mock_response"
@pytest.mark.skipif(
sys.version_info < (3, 11),
reason="Python 3.10 doesn't bound the original function provided to the wraps argument of the patch object.",
)
async def test_invoke_with_max_stall_count_exceeded():
""" "Test the invoke method of the MagenticOrchestration with max stall count exceeded."""
runtime = InProcessRuntime()
runtime.start()
with (
patch.object(MockAgent, "invoke_stream", wraps=MockAgent.invoke_stream, autospec=True) as mock_invoke_stream,
patch.object(
MockChatCompletionService, "get_chat_message_content", new_callable=AsyncMock
) as mock_get_chat_message_content,
patch.object(
StandardMagenticManager,
"create_progress_ledger",
new_callable=AsyncMock,
side_effect=ManagerProgressListStalling,
),
):
mock_get_chat_message_content.return_value = ChatMessageContent(role="assistant", content="mock_response")
agent_a = MockAgent(name="agent_a", description="test agent")
agent_b = MockAgent(name="agent_b", description="test agent")
try:
orchestration = MagenticOrchestration(
members=[agent_a, agent_b],
manager=StandardMagenticManager(
chat_completion_service=MockChatCompletionService(ai_model_id="test"),
prompt_execution_settings=MockPromptExecutionSettings(),
max_stall_count=1,
),
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
await orchestration_result.get(1.0)
finally:
await runtime.stop_when_idle()
assert mock_invoke_stream.call_count == 3
# Exceeding max stall count will trigger replanning, which will recreate the facts and plan,
# resulting in two additional calls to get_chat_message_content compared to the `test_invoke` test.
assert mock_get_chat_message_content.call_count == 5
@pytest.mark.skipif(
sys.version_info < (3, 11),
reason="Python 3.10 doesn't bound the original function provided to the wraps argument of the patch object.",
)
async def test_invoke_with_max_round_count_exceeded():
""" "Test the invoke method of the MagenticOrchestration with max round count exceeded."""
runtime = InProcessRuntime()
runtime.start()
with (
patch.object(MockAgent, "invoke_stream", wraps=MockAgent.invoke_stream, autospec=True) as mock_invoke_stream,
patch.object(
MockChatCompletionService, "get_chat_message_content", new_callable=AsyncMock
) as mock_get_chat_message_content,
patch.object(
StandardMagenticManager,
"create_progress_ledger",
new_callable=AsyncMock,
side_effect=ManagerProgressListStalling,
),
):
mock_get_chat_message_content.return_value = ChatMessageContent(role="assistant", content="mock_response")
agent_a = MockAgent(name="agent_a", description="test agent")
agent_b = MockAgent(name="agent_b", description="test agent")
try:
orchestration = MagenticOrchestration(
members=[agent_a, agent_b],
manager=StandardMagenticManager(
chat_completion_service=MockChatCompletionService(ai_model_id="test"),
prompt_execution_settings=MockPromptExecutionSettings(),
max_round_count=1,
),
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
result = await orchestration_result.get(1.0)
finally:
await runtime.stop_when_idle()
# Partial result will be returned when max round count is exceeded.
assert result.content == mock_get_chat_message_content.return_value.content
assert mock_invoke_stream.call_count == 1
# Planning will be called once, so the facts and plan will be created once.
assert mock_get_chat_message_content.call_count == 2
@pytest.mark.skipif(
sys.version_info < (3, 11),
reason="Python 3.10 doesn't bound the original function provided to the wraps argument of the patch object.",
)
async def test_invoke_with_max_reset_count_exceeded():
""" "Test the invoke method of the MagenticOrchestration with max reset count exceeded."""
runtime = InProcessRuntime()
runtime.start()
with (
patch.object(MockAgent, "invoke_stream", wraps=MockAgent.invoke_stream, autospec=True) as mock_invoke_stream,
patch.object(
MockChatCompletionService, "get_chat_message_content", new_callable=AsyncMock
) as mock_get_chat_message_content,
patch.object(
StandardMagenticManager,
"create_progress_ledger",
new_callable=AsyncMock,
side_effect=ManagerProgressListStalling,
),
):
mock_get_chat_message_content.return_value = ChatMessageContent(role="assistant", content="mock_response")
agent_a = MockAgent(name="agent_a", description="test agent")
agent_b = MockAgent(name="agent_b", description="test agent")
try:
orchestration = MagenticOrchestration(
members=[agent_a, agent_b],
manager=StandardMagenticManager(
chat_completion_service=MockChatCompletionService(ai_model_id="test"),
prompt_execution_settings=MockPromptExecutionSettings(),
max_stall_count=0, # No stall allowed
max_reset_count=0, # No reset allowed
),
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
result = await orchestration_result.get(1.0)
finally:
await runtime.stop_when_idle()
# Partial result will be returned when max reset count is exceeded. The test emits content based on the prompt
# so check that the content is not None and not an exact match to a mock response.
assert result.content is not None
assert mock_invoke_stream.call_count == 1
# Planning and replanning will be each called once, so the facts and plan will be created twice.
assert mock_get_chat_message_content.call_count == 4
@pytest.mark.skipif(
sys.version_info < (3, 11),
reason="Python 3.10 doesn't bound the original function provided to the wraps argument of the patch object.",
)
async def test_invoke_with_unknown_speaker():
"""Test the invoke method of the MagenticOrchestration with an unknown speaker."""
runtime = InProcessRuntime()
runtime.start()
with (
patch.object(
MockChatCompletionService, "get_chat_message_content", new_callable=AsyncMock
) as mock_get_chat_message_content,
patch.object(
StandardMagenticManager,
"create_progress_ledger",
new_callable=AsyncMock,
side_effect=ManagerProgressListUnknownSpeaker,
),
pytest.raises(ValueError),
):
mock_get_chat_message_content.return_value = ChatMessageContent(role="assistant", content="mock_response")
agent_a = MockAgent(name="agent_a", description="test agent")
agent_b = MockAgent(name="agent_b", description="test agent")
try:
orchestration = MagenticOrchestration(
members=[agent_a, agent_b],
manager=StandardMagenticManager(
chat_completion_service=MockChatCompletionService(ai_model_id="test"),
prompt_execution_settings=MockPromptExecutionSettings(),
),
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
await orchestration_result.get(1.0)
finally:
await runtime.stop_when_idle()
# endregion MagenticOrchestration
# region StandardMagenticManager
def test_standard_magentic_manager_init():
"""Test the initialization of the StandardMagenticManager."""
chat_completion_service = MockChatCompletionService(ai_model_id="test")
prompt_execution_settings = MockPromptExecutionSettings()
manager = StandardMagenticManager(
chat_completion_service=chat_completion_service,
prompt_execution_settings=prompt_execution_settings,
)
assert manager.max_stall_count > 0
assert manager.max_reset_count is None
assert manager.max_round_count is None
assert (
manager.task_ledger_facts_prompt is not None
and manager.task_ledger_facts_prompt == ORCHESTRATOR_TASK_LEDGER_FACTS_PROMPT
)
assert (
manager.task_ledger_plan_prompt is not None
and manager.task_ledger_plan_prompt == ORCHESTRATOR_TASK_LEDGER_PLAN_PROMPT
)
assert (
manager.task_ledger_full_prompt is not None
and manager.task_ledger_full_prompt == ORCHESTRATOR_TASK_LEDGER_FULL_PROMPT
)
assert (
manager.task_ledger_facts_update_prompt is not None
and manager.task_ledger_facts_update_prompt == ORCHESTRATOR_TASK_LEDGER_FACTS_UPDATE_PROMPT
)
assert (
manager.task_ledger_plan_update_prompt is not None
and manager.task_ledger_plan_update_prompt == ORCHESTRATOR_TASK_LEDGER_PLAN_UPDATE_PROMPT
)
assert (
manager.progress_ledger_prompt is not None
and manager.progress_ledger_prompt == ORCHESTRATOR_PROGRESS_LEDGER_PROMPT
)
assert manager.final_answer_prompt is not None and manager.final_answer_prompt == ORCHESTRATOR_FINAL_ANSWER_PROMPT
def test_standard_magentic_manager_init_with_custom_prompts():
"""Test the initialization of the StandardMagenticManager with custom prompts."""
chat_completion_service = MockChatCompletionService(ai_model_id="test")
prompt_execution_settings = MockPromptExecutionSettings()
manager = StandardMagenticManager(
chat_completion_service=chat_completion_service,
prompt_execution_settings=prompt_execution_settings,
task_ledger_facts_prompt="custom_task_ledger_facts_prompt",
task_ledger_plan_prompt="custom_task_ledger_plan_prompt",
task_ledger_full_prompt="custom_task_ledger_full_prompt",
task_ledger_facts_update_prompt="custom_task_ledger_facts_update_prompt",
task_ledger_plan_update_prompt="custom_task_ledger_plan_update_prompt",
progress_ledger_prompt="custom_progress_ledger_prompt",
final_answer_prompt="custom_final_answer_prompt",
)
assert manager.task_ledger_facts_prompt == "custom_task_ledger_facts_prompt"
assert manager.task_ledger_plan_prompt == "custom_task_ledger_plan_prompt"
assert manager.task_ledger_full_prompt == "custom_task_ledger_full_prompt"
assert manager.task_ledger_facts_update_prompt == "custom_task_ledger_facts_update_prompt"
assert manager.task_ledger_plan_update_prompt == "custom_task_ledger_plan_update_prompt"
assert manager.progress_ledger_prompt == "custom_progress_ledger_prompt"
assert manager.final_answer_prompt == "custom_final_answer_prompt"
def test_standard_magentic_manager_init_with_invalid_prompt_execution_settings():
"""Test the initialization of the StandardMagenticManager with invalid prompt execution settings."""
chat_completion_service = MockChatCompletionService(ai_model_id="test")
prompt_execution_settings = PromptExecutionSettings()
with pytest.raises(ValueError):
StandardMagenticManager(
chat_completion_service=chat_completion_service,
prompt_execution_settings=prompt_execution_settings,
)
def test_standard_magentic_manager_init_without_prompt_execution_settings():
"""Test the initialization of the StandardMagenticManager without prompt execution settings."""
# The default prompt execution settings of the mock chat completion service
# does not support structured output.
chat_completion_service = MockChatCompletionService(ai_model_id="test")
with pytest.raises(ValueError):
StandardMagenticManager(chat_completion_service=chat_completion_service)
async def test_standard_magentic_manager_plan():
"""Test the plan method of the StandardMagenticManager."""
with patch.object(
MockChatCompletionService, "get_chat_message_content", new_callable=AsyncMock
) as mock_get_chat_message_content:
mock_get_chat_message_content.return_value = ChatMessageContent(role="assistant", content="mock_response")
chat_completion_service = MockChatCompletionService(ai_model_id="test")
prompt_execution_settings = MockPromptExecutionSettings()
manager = StandardMagenticManager(
chat_completion_service=chat_completion_service,
prompt_execution_settings=prompt_execution_settings,
task_ledger_facts_prompt="custom_task_ledger_facts_prompt",
task_ledger_plan_prompt="custom_task_ledger_plan_prompt {{$team}}",
)
magentic_context = MagenticContext(
chat_history=ChatHistory(),
task=ChatMessageContent(role="user", content="test_message"),
participant_descriptions={"agent_a": "test_agent_a", "agent_b": "test_agent_b"},
)
task_ledger = await manager.plan(magentic_context.model_copy(deep=True))
assert isinstance(task_ledger, ChatMessageContent)
assert task_ledger.content.count("mock_response") == 2
assert "test_message" in task_ledger.content
assert "{'agent_a': 'test_agent_a', 'agent_b': 'test_agent_b'}" in task_ledger.content
assert mock_get_chat_message_content.call_count == 2
assert (
mock_get_chat_message_content.call_args_list[0][0][0].messages[0].content
== "custom_task_ledger_facts_prompt"
)
assert (
mock_get_chat_message_content.call_args_list[1][0][0].messages[2].content
== "custom_task_ledger_plan_prompt {'agent_a': 'test_agent_a', 'agent_b': 'test_agent_b'}"
)
async def test_standard_magentic_manager_replan():
"""Test the replan method of the StandardMagenticManager."""
with patch.object(
MockChatCompletionService, "get_chat_message_content", new_callable=AsyncMock
) as mock_get_chat_message_content:
mock_get_chat_message_content.return_value = ChatMessageContent(role="assistant", content="mock_response")
chat_completion_service = MockChatCompletionService(ai_model_id="test")
prompt_execution_settings = MockPromptExecutionSettings()
manager = StandardMagenticManager(
chat_completion_service=chat_completion_service,
prompt_execution_settings=prompt_execution_settings,
task_ledger_facts_update_prompt="custom_task_ledger_facts_prompt {{$old_facts}}",
task_ledger_plan_update_prompt="custom_task_ledger_plan_prompt {{$team}}",
)
magentic_context = MagenticContext(
chat_history=ChatHistory(),
task=ChatMessageContent(role="user", content="test_message"),
participant_descriptions={"agent_a": "test_agent_a", "agent_b": "test_agent_b"},
)
# Need to plan before replanning
_ = await manager.plan(magentic_context.model_copy(deep=True))
task_ledger = await manager.replan(magentic_context.model_copy(deep=True))
assert isinstance(task_ledger, ChatMessageContent)
assert task_ledger.content.count("mock_response") == 2
assert "test_message" in task_ledger.content
assert "{'agent_a': 'test_agent_a', 'agent_b': 'test_agent_b'}" in task_ledger.content
assert mock_get_chat_message_content.call_count == 4
assert (
mock_get_chat_message_content.call_args_list[2][0][0].messages[0].content
== "custom_task_ledger_facts_prompt mock_response"
)
assert (
mock_get_chat_message_content.call_args_list[3][0][0].messages[2].content
== "custom_task_ledger_plan_prompt {'agent_a': 'test_agent_a', 'agent_b': 'test_agent_b'}"
)
async def test_standard_magentic_manager_replan_without_plan():
"""Test the replan method of the StandardMagenticManager."""
chat_completion_service = MockChatCompletionService(ai_model_id="test")
prompt_execution_settings = MockPromptExecutionSettings()
manager = StandardMagenticManager(
chat_completion_service=chat_completion_service,
prompt_execution_settings=prompt_execution_settings,
)
magentic_context = MagenticContext(
chat_history=ChatHistory(),
task=ChatMessageContent(role="user", content="test_message"),
participant_descriptions={"agent_a": "test_agent_a", "agent_b": "test_agent_b"},
)
with pytest.raises(RuntimeError):
_ = await manager.replan(magentic_context.model_copy(deep=True))
async def test_standard_magentic_manager_create_progress_ledger():
"""Test the create_progress_ledger method of the StandardMagenticManager."""
mock_progress_ledger = ProgressLedger(
is_request_satisfied=ProgressLedgerItem(answer=False, reason="mock_reasoning"),
is_in_loop=ProgressLedgerItem(answer=False, reason="mock_reasoning"),
is_progress_being_made=ProgressLedgerItem(answer=False, reason="mock_reasoning"),
next_speaker=ProgressLedgerItem(answer="agent_a", reason="mock_reasoning"),
instruction_or_question=ProgressLedgerItem(answer="mock_instruction", reason="mock_reasoning"),
)
with patch.object(
MockChatCompletionService, "get_chat_message_content", new_callable=AsyncMock
) as mock_get_chat_message_content:
mock_get_chat_message_content.return_value = ChatMessageContent(
role="assistant", content=mock_progress_ledger.model_dump_json()
)
chat_completion_service = MockChatCompletionService(ai_model_id="test")
prompt_execution_settings = MockPromptExecutionSettings()
manager = StandardMagenticManager(
chat_completion_service=chat_completion_service,
prompt_execution_settings=prompt_execution_settings,
)
magentic_context = MagenticContext(
chat_history=ChatHistory(),
task=ChatMessageContent(role="user", content="test_message"),
participant_descriptions={"agent_a": "test_agent_a", "agent_b": "test_agent_b"},
)
progress_ledger = await manager.create_progress_ledger(magentic_context.model_copy(deep=True))
assert isinstance(progress_ledger, ProgressLedger)
assert progress_ledger == mock_progress_ledger
assert (
"{'agent_a': 'test_agent_a', 'agent_b': 'test_agent_b'}"
in mock_get_chat_message_content.call_args_list[0][0][0].messages[0].content
)
assert "agent_a, agent_b" in mock_get_chat_message_content.call_args_list[0][0][0].messages[0].content
assert (
magentic_context.task.content in mock_get_chat_message_content.call_args_list[0][0][0].messages[0].content
)
assert mock_get_chat_message_content.call_args_list[0][0][1].extension_data["response_format"] == ProgressLedger
# endregion MagenticManager
@@ -0,0 +1,422 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import json
import sys
from collections.abc import AsyncIterable, Awaitable, Callable
from dataclasses import dataclass
from unittest.mock import ANY, patch
import pytest
from semantic_kernel.agents.agent import Agent, AgentResponseItem, AgentThread
from semantic_kernel.agents.orchestration.orchestration_base import DefaultTypeAlias, OrchestrationBase, TIn, TOut
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.kernel_pydantic import KernelBaseModel
from tests.unit.agents.orchestration.conftest import MockRuntime
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
class MockAgent(Agent):
"""A mock agent for testing purposes."""
@override
async def get_response(
self,
*,
messages: str | ChatMessageContent | list[str | ChatMessageContent] | None = None,
thread: AgentThread | None = None,
**kwargs,
) -> AgentResponseItem[ChatMessageContent]:
pass
@override
async def invoke(
self,
*,
messages: str | ChatMessageContent | list[str | ChatMessageContent] | None = None,
thread: AgentThread | None = None,
on_intermediate_message: Callable[[ChatMessageContent], Awaitable[None]] | None = None,
**kwargs,
) -> AgentResponseItem[ChatMessageContent]:
pass
@override
async def invoke_stream(
self,
*,
messages: str | ChatMessageContent | list[str | ChatMessageContent] | None = None,
thread: AgentThread | None = None,
on_intermediate_message: Callable[[ChatMessageContent], Awaitable[None]] | None = None,
**kwargs,
) -> AsyncIterable[AgentResponseItem[StreamingChatMessageContent]]:
pass
class MockOrchestration(OrchestrationBase[TIn, TOut]):
"""A mock orchestration base for testing purposes."""
async def _start(self, task, runtime, internal_topic_type, collection_agent_type):
pass
async def _prepare(self, runtime, internal_topic_type, exception_callback, result_callback):
pass
def test_orchestration_init():
"""Test the initialization of the MockOrchestration."""
agent_a = MockAgent()
agent_b = MockAgent()
agent_c = MockAgent()
orchestration = MockOrchestration(
members=[agent_a, agent_b, agent_c],
name="test_orchestration",
description="Test Orchestration",
)
assert orchestration.name == "test_orchestration"
assert orchestration.description == "Test Orchestration"
assert len(orchestration._members) == 3
assert orchestration._input_transform is not None
assert orchestration._output_transform is not None
assert orchestration._agent_response_callback is None
def test_orchestration_init_with_default_values():
"""Test the initialization of the MockOrchestration with default values."""
agent_a = MockAgent()
agent_b = MockAgent()
orchestration = MockOrchestration(members=[agent_a, agent_b])
assert orchestration.name
assert orchestration.description
assert len(orchestration._members) == 2
assert orchestration._input_transform is not None
assert orchestration._output_transform is not None
assert orchestration._agent_response_callback is None
def test_orchestration_init_with_no_members():
"""Test the initialization of the OrchestrationBase with no members."""
with pytest.raises(ValueError):
_ = MockOrchestration(members=[])
def test_orchestration_set_types():
"""Test the set_types method of OrchestrationBase."""
agent_a = MockAgent()
agent_b = MockAgent()
# Test with default types
orchestration_a = MockOrchestration(members=[agent_a, agent_b])
orchestration_a._set_types()
assert orchestration_a.t_in is DefaultTypeAlias
assert orchestration_a.t_out is DefaultTypeAlias
# Test with a custom input type and default output type
orchestration_c = MockOrchestration[int](members=[agent_a, agent_b])
orchestration_c._set_types()
assert orchestration_c.t_in is int
assert orchestration_c.t_out is DefaultTypeAlias
# Test with a custom input type and custom output type
orchestration_b = MockOrchestration[str, int](members=[agent_a, agent_b])
orchestration_b._set_types()
assert orchestration_b.t_in is str
assert orchestration_b.t_out is int
# Test with an incorrect number of types
with pytest.raises(TypeError):
orchestration_d = MockOrchestration[str, str, str](members=[agent_a, agent_b])
orchestration_d._set_types()
async def test_orchestration_invoke_with_str():
"""Test the invoke method of OrchestrationBase with a string input."""
orchestration = MockOrchestration(members=[MockAgent(), MockAgent()])
with patch.object(orchestration, "_start") as mock_start:
await orchestration.invoke("Test message", MockRuntime())
mock_start.assert_called_once_with(
ChatMessageContent(role=AuthorRole.USER, content="Test message"), ANY, ANY, ANY
)
async def test_orchestration_invoke_with_chat_message_content():
"""Test the invoke method of OrchestrationBase with a ChatMessageContent input."""
orchestration = MockOrchestration(members=[MockAgent(), MockAgent()])
chat_message_content = ChatMessageContent(role=AuthorRole.USER, content="Test message")
with patch.object(orchestration, "_start") as mock_start:
await orchestration.invoke(chat_message_content, MockRuntime())
mock_start.assert_called_once_with(chat_message_content, ANY, ANY, ANY)
chat_message_content_list = [
ChatMessageContent(role=AuthorRole.USER, content="Test message 1"),
ChatMessageContent(role=AuthorRole.USER, content="Test message 2"),
]
with patch.object(orchestration, "_start") as mock_start:
await orchestration.invoke(chat_message_content_list, MockRuntime())
mock_start.assert_called_once_with(chat_message_content_list, ANY, ANY, ANY)
async def test_orchestration_invoke_with_custom_type():
"""Test the invoke method of OrchestrationBase with a custom type."""
@dataclass
class CustomType:
value: str
number: int
orchestration = MockOrchestration[CustomType, TOut](members=[MockAgent(), MockAgent()])
custom_type_instance = CustomType(value="Test message", number=42)
with patch.object(orchestration, "_start") as mock_start:
await orchestration.invoke(custom_type_instance, MockRuntime())
mock_start.assert_called_once_with(
ChatMessageContent(role=AuthorRole.USER, content=json.dumps(custom_type_instance.__dict__)), ANY, ANY, ANY
)
async def test_orchestration_invoke_with_custom_type_async_input_transform():
"""Test the invoke method of OrchestrationBase with a custom type and async input transform."""
@dataclass
class CustomType:
value: str
number: int
async def async_input_transform(input_data: CustomType) -> ChatMessageContent:
await asyncio.sleep(0.1) # Simulate async processing
return ChatMessageContent(role=AuthorRole.USER, content="Test message")
orchestration = MockOrchestration[CustomType, TOut](
members=[MockAgent(), MockAgent()],
input_transform=async_input_transform,
)
custom_type_instance = CustomType(value="Test message", number=42)
with patch.object(orchestration, "_start") as mock_start:
await orchestration.invoke(custom_type_instance, MockRuntime())
mock_start.assert_called_once_with(
ChatMessageContent(role=AuthorRole.USER, content="Test message"), ANY, ANY, ANY
)
def test_default_input_transform_default_type_alias():
"""Test the default_input_transform method of OrchestrationBase."""
orchestration = MockOrchestration(members=[MockAgent(), MockAgent()])
orchestration._set_types()
chat_message_content = ChatMessageContent(role=AuthorRole.USER, content="Test message")
transformed_input = orchestration._default_input_transform(chat_message_content)
assert isinstance(transformed_input, ChatMessageContent)
chat_message_content_list = [
ChatMessageContent(role=AuthorRole.USER, content="Test message 1"),
ChatMessageContent(role=AuthorRole.USER, content="Test message 2"),
]
transformed_input_list = orchestration._default_input_transform(chat_message_content_list)
assert isinstance(transformed_input_list, list) and all(
isinstance(item, ChatMessageContent) for item in transformed_input_list
)
def test_default_input_transform_custom_type():
"""Test the default_input_transform method of OrchestrationBase with a custom type."""
@dataclass
class CustomType:
value: str
number: int
orchestration_a = MockOrchestration[CustomType, TOut](members=[MockAgent(), MockAgent()])
orchestration_a._set_types()
custom_type_instance = CustomType(value="Test message", number=42)
transformed_input_a = orchestration_a._default_input_transform(custom_type_instance)
assert isinstance(transformed_input_a, ChatMessageContent)
assert CustomType(**json.loads(transformed_input_a.content)) == custom_type_instance
assert transformed_input_a.role == AuthorRole.USER
class CustomModel(KernelBaseModel):
value: str
number: int
orchestration_b = MockOrchestration[CustomModel, TOut](members=[MockAgent(), MockAgent()])
orchestration_b._set_types()
custom_model_instance = CustomModel(value="Test message", number=42)
transformed_input_b = orchestration_b._default_input_transform(custom_model_instance)
assert isinstance(transformed_input_b, ChatMessageContent)
assert CustomModel.model_validate_json(transformed_input_b.content) == custom_model_instance
assert transformed_input_b.role == AuthorRole.USER
def test_default_input_transform_custom_type_error():
"""Test the default_input_transform method of OrchestrationBase with an incorrect type."""
@dataclass
class CustomType:
value: str
number: int
class CustomModel(KernelBaseModel):
value: str
number: int
orchestration = MockOrchestration[CustomModel, TOut](members=[MockAgent(), MockAgent()])
orchestration._set_types()
with pytest.raises(TypeError):
custom_type_instance = CustomType(value="Test message", number=42)
orchestration._default_input_transform(custom_type_instance)
def test_default_output_transform_default_type_alias():
"""Test the default_output_transform method of OrchestrationBase."""
orchestration = MockOrchestration(members=[MockAgent(), MockAgent()])
orchestration._set_types()
chat_message_content = ChatMessageContent(role=AuthorRole.USER, content="Test message")
transformed_output = orchestration._default_output_transform(chat_message_content)
assert isinstance(transformed_output, ChatMessageContent)
chat_message_content_list = [
ChatMessageContent(role=AuthorRole.USER, content="Test message 1"),
ChatMessageContent(role=AuthorRole.USER, content="Test message 2"),
]
transformed_output_list = orchestration._default_output_transform(chat_message_content_list)
assert isinstance(transformed_output_list, list) and all(
isinstance(item, ChatMessageContent) for item in transformed_output_list
)
with pytest.raises(TypeError, match="Invalid output message type"):
orchestration._default_output_transform("Invalid type")
def test_default_output_transform_custom_type():
"""Test the default_output_transform method of OrchestrationBase with a custom type."""
@dataclass
class CustomType:
value: str
number: int
orchestration_a = MockOrchestration[TIn, CustomType](members=[MockAgent(), MockAgent()])
orchestration_a._set_types()
custom_type_instance = CustomType(value="Test message", number=42)
chat_message_content = ChatMessageContent(role=AuthorRole.USER, content=json.dumps(custom_type_instance.__dict__))
transformed_output_a = orchestration_a._default_output_transform(chat_message_content)
assert isinstance(transformed_output_a, CustomType)
assert transformed_output_a == custom_type_instance
class CustomModel(KernelBaseModel):
value: str
number: int
orchestration_b = MockOrchestration[TIn, CustomModel](members=[MockAgent(), MockAgent()])
orchestration_b._set_types()
custom_model_instance = CustomModel(value="Test message", number=42)
chat_message_content = ChatMessageContent(role=AuthorRole.USER, content=custom_model_instance.model_dump_json())
transformed_output_b = orchestration_b._default_output_transform(chat_message_content)
assert isinstance(transformed_output_b, CustomModel)
assert transformed_output_b == custom_model_instance
def test_default_output_transform_custom_type_error():
"""Test the default_output_transform method of OrchestrationBase with an incorrect type."""
@dataclass
class CustomType:
value: str
number: int
class CustomModel(KernelBaseModel):
value: str
number: int
orchestration = MockOrchestration[TIn, CustomModel](members=[MockAgent(), MockAgent()])
orchestration._set_types()
with pytest.raises(TypeError, match="Unable to transform output message"):
custom_type_instance = CustomType(value="Test message", number=42)
orchestration._default_output_transform(custom_type_instance)
async def test_invoke_with_timeout_error():
"""Test the invoke method of the MockOrchestration with a timeout error."""
agent_a = MockAgent()
agent_b = MockAgent()
orchestration = MockOrchestration(members=[agent_a, agent_b])
# The orchestration_result will never be set by the MockOrchestration
orchestration_result = await orchestration.invoke(
task="test_message",
runtime=MockRuntime(),
)
with pytest.raises(asyncio.TimeoutError):
await orchestration_result.get(timeout=0.1)
async def test_invoke_cancel_before_completion():
"""Test the invoke method of the MockOrchestration with cancellation before completion."""
agent_a = MockAgent()
agent_b = MockAgent()
orchestration = MockOrchestration(members=[agent_a, agent_b])
# The orchestration_result will never be set by the MockOrchestration
orchestration_result = await orchestration.invoke(
task="test_message",
runtime=MockRuntime(),
)
# Cancel the orchestration before completion
orchestration_result.cancel()
with pytest.raises(RuntimeError, match="The invocation was canceled before it could complete."):
await orchestration_result.get()
async def test_invoke_with_double_cancel():
"""Test the invoke method of the MockOrchestration with double cancel."""
agent_a = MockAgent()
agent_b = MockAgent()
orchestration = MockOrchestration(members=[agent_a, agent_b])
# The orchestration_result will never be set by the MockOrchestration
orchestration_result = await orchestration.invoke(
task="test_message",
runtime=MockRuntime(),
)
orchestration_result.cancel()
# Cancelling again should raise an error
with pytest.raises(RuntimeError, match="The invocation has already been canceled."):
orchestration_result.cancel()
@@ -0,0 +1,173 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
from collections.abc import Callable
from typing import Any, Literal
from unittest.mock import AsyncMock, patch
import pytest
from pydantic import BaseModel
from semantic_kernel.agents.orchestration.tools import structured_outputs_transform
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.kernel_pydantic import KernelBaseModel
if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
from typing_extensions import override # pragma: no cover
class MockPromptExecutionSettings(PromptExecutionSettings):
"""A mock prompt execution settings class for testing purposes."""
response_format: (
dict[Literal["type"], Literal["text", "json_object"]] | dict[str, Any] | type[BaseModel] | type | None
) = None
class MockChatCompletionService(ChatCompletionClientBase):
"""A mock chat completion service for testing purposes."""
@override
def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]:
"""Get the request settings class."""
return MockPromptExecutionSettings
class MockModel(KernelBaseModel):
name: str
age: int
def test_structured_outputs_transform():
"""Test the structured_outputs_transform function."""
service = MockChatCompletionService(ai_model_id="test_model")
prompt_execution_settings = PromptExecutionSettings()
transform = structured_outputs_transform(
target_structure=MockModel,
service=service,
prompt_execution_settings=prompt_execution_settings,
)
assert isinstance(transform, Callable)
def test_structured_outputs_transform_original_settings_not_changed():
"""Test the structured_outputs_transform function with original settings not changed."""
service = MockChatCompletionService(ai_model_id="test_model")
prompt_execution_settings = PromptExecutionSettings(
extension_data={"test_key": "test_value"},
)
_ = structured_outputs_transform(
target_structure=MockModel,
service=service,
prompt_execution_settings=prompt_execution_settings,
)
assert not hasattr(prompt_execution_settings, "response_format")
assert prompt_execution_settings.extension_data["test_key"] == "test_value"
def test_structured_outputs_transform_unsupported_service():
"""Test the structured_outputs_transform function with unsupported service."""
with (
patch.object(
MockChatCompletionService, "get_prompt_execution_settings_class"
) as mock_get_prompt_execution_settings_class,
pytest.raises(ValueError),
):
mock_get_prompt_execution_settings_class.return_value = PromptExecutionSettings
service = MockChatCompletionService(ai_model_id="test_model")
prompt_execution_settings = PromptExecutionSettings()
_ = structured_outputs_transform(MockModel, service, prompt_execution_settings)
async def test_structured_outputs_transform_invoke():
"""Test the structured_outputs_transform function and invoke the transform."""
mock_model = MockModel(name="John Doe", age=30)
with (
patch.object(
MockChatCompletionService, "get_chat_message_content", new_callable=AsyncMock
) as mock_get_chat_message_content,
):
mock_get_chat_message_content.return_value = ChatMessageContent(
role="assistant", content=mock_model.model_dump_json()
)
service = MockChatCompletionService(ai_model_id="test_model")
prompt_execution_settings = PromptExecutionSettings()
transform = structured_outputs_transform(
target_structure=MockModel,
service=service,
prompt_execution_settings=prompt_execution_settings,
)
result = await transform(ChatMessageContent(role="user", content="name is John Doe and age is 30"))
assert isinstance(result, MockModel)
assert result == mock_model
mock_get_chat_message_content.assert_called_once()
assert len(mock_get_chat_message_content.call_args[0][0].messages) == 2
async def test_structured_outputs_transform_invoke_with_messages():
"""Test the structured_outputs_transform function and invoke the transform with messages."""
mock_model = MockModel(name="John Doe", age=30)
with (
patch.object(
MockChatCompletionService, "get_chat_message_content", new_callable=AsyncMock
) as mock_get_chat_message_content,
):
mock_get_chat_message_content.return_value = ChatMessageContent(
role="assistant", content=mock_model.model_dump_json()
)
service = MockChatCompletionService(ai_model_id="test_model")
prompt_execution_settings = PromptExecutionSettings()
transform = structured_outputs_transform(
target_structure=MockModel,
service=service,
prompt_execution_settings=prompt_execution_settings,
)
result = await transform([
ChatMessageContent(role="user", content="name is John Doe"),
ChatMessageContent(role="user", content="age is 30"),
])
assert isinstance(result, MockModel)
assert result == mock_model
mock_get_chat_message_content.assert_called_once()
assert len(mock_get_chat_message_content.call_args[0][0].messages) == 3
async def test_structured_outputs_transform_invoke_unsupported_type():
"""Test the structured_outputs_transform function and invoke the transform with messages of unsupported type."""
service = MockChatCompletionService(ai_model_id="test_model")
prompt_execution_settings = PromptExecutionSettings()
transform = structured_outputs_transform(
target_structure=MockModel,
service=service,
prompt_execution_settings=prompt_execution_settings,
)
with pytest.raises(ValueError):
_ = await transform("unsupported type")
@@ -0,0 +1,202 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import sys
from unittest.mock import patch
import pytest
from semantic_kernel.agents.orchestration.orchestration_base import DefaultTypeAlias, OrchestrationResult
from semantic_kernel.agents.orchestration.sequential import SequentialOrchestration
from semantic_kernel.agents.runtime.in_process.in_process_runtime import InProcessRuntime
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent
from tests.unit.agents.orchestration.conftest import MockAgent, MockAgentWithException, MockRuntime
async def test_prepare():
"""Test the prepare method of the SequentialOrchestration."""
agent_a = MockAgent()
agent_b = MockAgent()
runtime = MockRuntime()
package_path = "semantic_kernel.agents.orchestration.sequential"
with (
patch(f"{package_path}.SequentialOrchestration._start"),
patch(f"{package_path}.SequentialAgentActor.register") as mock_agent_actor_register,
patch(f"{package_path}.CollectionActor.register") as mock_collection_actor_register,
patch.object(runtime, "add_subscription") as mock_add_subscription,
):
orchestration = SequentialOrchestration(members=[agent_a, agent_b])
await orchestration.invoke(task="test_message", runtime=runtime)
assert mock_agent_actor_register.call_count == 2
assert mock_collection_actor_register.call_count == 1
assert mock_add_subscription.call_count == 0
async def test_invoke():
"""Test the invoke method of the SequentialOrchestration."""
agent_a = MockAgent()
agent_b = MockAgent()
runtime = InProcessRuntime()
runtime.start()
try:
orchestration = SequentialOrchestration(members=[agent_a, agent_b])
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
result = await orchestration_result.get(1.0)
assert isinstance(orchestration_result, OrchestrationResult)
assert isinstance(result, ChatMessageContent)
finally:
await runtime.stop_when_idle()
async def test_invoke_with_response_callback():
"""Test the invoke method of the SequentialOrchestration with a response callback."""
agent_a = MockAgent()
agent_b = MockAgent()
runtime = InProcessRuntime()
runtime.start()
responses: list[DefaultTypeAlias] = []
try:
orchestration = SequentialOrchestration(
members=[agent_a, agent_b],
agent_response_callback=lambda x: responses.append(x),
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
await orchestration_result.get(1.0)
assert len(responses) == 2
assert all(isinstance(item, ChatMessageContent) for item in responses)
assert all(item.content == "mock_response" for item in responses)
finally:
await runtime.stop_when_idle()
async def test_invoke_with_streaming_response_callback():
"""Test the invoke method of the SequentialOrchestration with a streaming response callback."""
agent_a = MockAgent()
agent_b = MockAgent()
runtime = InProcessRuntime()
runtime.start()
responses: dict[str, list[StreamingChatMessageContent]] = {}
try:
orchestration = SequentialOrchestration(
members=[agent_a, agent_b],
streaming_agent_response_callback=lambda x, _: responses.setdefault(x.name, []).append(x),
)
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
await orchestration_result.get(1.0)
assert len(responses[agent_a.name]) == 2
assert len(responses[agent_b.name]) == 2
assert all(isinstance(item, StreamingChatMessageContent) for item in responses[agent_a.name])
assert all(isinstance(item, StreamingChatMessageContent) for item in responses[agent_b.name])
agent_a_response = sum(responses[agent_a.name][1:], responses[agent_a.name][0])
assert agent_a_response.content == "mock_response"
agent_b_response = sum(responses[agent_b.name][1:], responses[agent_b.name][0])
assert agent_b_response.content == "mock_response"
finally:
await runtime.stop_when_idle()
@pytest.mark.skipif(
sys.version_info < (3, 11),
reason="Python 3.10 doesn't bound the original function provided to the wraps argument of the patch object.",
)
async def test_invoke_cancel_before_completion():
"""Test the invoke method of the SequentialOrchestration with cancellation before completion."""
with (
patch.object(MockAgent, "invoke_stream", wraps=MockAgent.invoke_stream, autospec=True) as mock_invoke_stream,
):
agent_a = MockAgent()
agent_b = MockAgent()
runtime = InProcessRuntime()
runtime.start()
try:
orchestration = SequentialOrchestration(members=[agent_a, agent_b])
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
# Cancel while the first agent is processing
await asyncio.sleep(0.05)
orchestration_result.cancel()
finally:
await runtime.stop_when_idle()
assert mock_invoke_stream.call_count == 1
async def test_invoke_cancel_after_completion():
"""Test the invoke method of the SequentialOrchestration with cancellation after completion."""
agent_a = MockAgent()
agent_b = MockAgent()
runtime = InProcessRuntime()
runtime.start()
try:
orchestration = SequentialOrchestration(members=[agent_a, agent_b])
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
# Wait for the orchestration to complete
await orchestration_result.get(1.0)
with pytest.raises(RuntimeError, match="The invocation has already been completed."):
orchestration_result.cancel()
finally:
await runtime.stop_when_idle()
async def test_invoke_with_double_get_result():
"""Test the invoke method of the SequentialOrchestration with double get result."""
agent_a = MockAgent()
agent_b = MockAgent()
runtime = InProcessRuntime()
runtime.start()
try:
orchestration = SequentialOrchestration(members=[agent_a, agent_b])
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
# Get result before completion
with pytest.raises(asyncio.TimeoutError):
await orchestration_result.get(0.1)
# The invocation should still be in progress and getting the result again should not raise an error
result = await orchestration_result.get(1.0)
assert isinstance(result, ChatMessageContent)
assert result.content == "mock_response"
finally:
await runtime.stop_when_idle()
async def test_invoke_with_agent_raising_exception():
"""Test the invoke method of the SequentialOrchestration with an agent raising an exception."""
agent_a = MockAgent()
agent_b = MockAgentWithException()
runtime = InProcessRuntime()
runtime.start()
try:
orchestration = SequentialOrchestration(members=[agent_a, agent_b])
orchestration_result = await orchestration.invoke(task="test_message", runtime=runtime)
with pytest.raises(RuntimeError, match="Mock agent exception"):
await orchestration_result.get(1.0)
assert orchestration_result.exception is not None
finally:
await runtime.stop_when_idle()