# Copyright (c) Microsoft. All rights reserved. import logging import sys from collections.abc import Awaitable, Callable from semantic_kernel.agents.agent import Agent from semantic_kernel.agents.orchestration.agent_actor_base import ActorBase, AgentActorBase from semantic_kernel.agents.orchestration.orchestration_base import DefaultTypeAlias, OrchestrationBase, TIn, TOut from semantic_kernel.agents.runtime.core.cancellation_token import CancellationToken from semantic_kernel.agents.runtime.core.core_runtime import CoreRuntime from semantic_kernel.agents.runtime.core.message_context import MessageContext from semantic_kernel.agents.runtime.core.routed_agent import message_handler from semantic_kernel.contents.chat_message_content import ChatMessageContent from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent from semantic_kernel.kernel_pydantic import KernelBaseModel from semantic_kernel.utils.feature_stage_decorator import experimental if sys.version_info >= (3, 12): from typing import override # pragma: no cover else: from typing_extensions import override # pragma: no cover logger: logging.Logger = logging.getLogger(__name__) @experimental class SequentialRequestMessage(KernelBaseModel): """A request message type for sequential agents.""" body: DefaultTypeAlias @experimental class SequentialResultMessage(KernelBaseModel): """A result message type for sequential agents.""" body: ChatMessageContent @experimental class SequentialAgentActor(AgentActorBase): """A agent actor for sequential agents that process tasks.""" def __init__( self, agent: Agent, internal_topic_type: str, next_agent_type: str, exception_callback: Callable[[BaseException], None], agent_response_callback: Callable[[DefaultTypeAlias], Awaitable[None] | None] | None = None, streaming_agent_response_callback: Callable[[StreamingChatMessageContent, bool], Awaitable[None] | None] | None = None, ) -> None: """Initialize the agent actor.""" self._next_agent_type = next_agent_type super().__init__( agent=agent, internal_topic_type=internal_topic_type, exception_callback=exception_callback, agent_response_callback=agent_response_callback, streaming_agent_response_callback=streaming_agent_response_callback, ) @message_handler async def _handle_message(self, message: SequentialRequestMessage, ctx: MessageContext) -> None: """Handle a message.""" logger.debug(f"Sequential actor (Actor ID: {self.id}; Agent name: {self._agent.name}) started processing...") response = await self._invoke_agent(additional_messages=message.body) logger.debug(f"Sequential actor (Actor ID: {self.id}; Agent name: {self._agent.name}) finished processing.") target_actor_id = await self.runtime.get(self._next_agent_type) await self.send_message( SequentialRequestMessage(body=response), target_actor_id, cancellation_token=ctx.cancellation_token, ) @experimental class CollectionActor(ActorBase): """A agent container for collection results from the last agent in the sequence.""" def __init__( self, description: str, exception_callback: Callable[[BaseException], None], result_callback: Callable[[DefaultTypeAlias], Awaitable[None]], ) -> None: """Initialize the collection actor.""" self._result_callback = result_callback super().__init__(description, exception_callback) @message_handler async def _handle_message(self, message: SequentialRequestMessage, _: MessageContext) -> None: """Handle the last message.""" await self._result_callback(message.body) @experimental class SequentialOrchestration(OrchestrationBase[TIn, TOut]): """A sequential multi-agent pattern orchestration.""" @override async def _start( self, task: DefaultTypeAlias, runtime: CoreRuntime, internal_topic_type: str, cancellation_token: CancellationToken, ) -> None: """Start the sequential pattern.""" target_actor_id = await runtime.get(self._get_agent_actor_type(self._members[0], internal_topic_type)) await runtime.send_message( SequentialRequestMessage(body=task), target_actor_id, cancellation_token=cancellation_token, ) @override async def _prepare( self, runtime: CoreRuntime, internal_topic_type: str, exception_callback: Callable[[BaseException], None], result_callback: Callable[[DefaultTypeAlias], Awaitable[None]], ) -> None: """Register the actors and orchestrations with the runtime and add the required subscriptions.""" await self._register_members(runtime, internal_topic_type, exception_callback) await self._register_collection_actor(runtime, internal_topic_type, exception_callback, result_callback) async def _register_members( self, runtime: CoreRuntime, internal_topic_type: str, exception_callback: Callable[[BaseException], None], ) -> None: """Register the members. The members will be registered in the reverse order so that the actor type of the next worker is available when the current worker is registered. This is important for the sequential orchestration, where actors need to know its next actor type to send the message to. Args: runtime (CoreRuntime): The agent runtime. internal_topic_type (str): The internal topic type for the orchestration that this actor is part of. exception_callback (Callable[[BaseException], None]): A callback function to handle exceptions. Returns: str: The first actor type in the sequence. """ next_actor_type = self._get_collection_actor_type(internal_topic_type) for agent in reversed(self._members): await SequentialAgentActor.register( runtime, self._get_agent_actor_type(agent, internal_topic_type), lambda agent=agent, next_actor_type=next_actor_type: SequentialAgentActor( # type: ignore[misc] agent, internal_topic_type, next_agent_type=next_actor_type, exception_callback=exception_callback, agent_response_callback=self._agent_response_callback, streaming_agent_response_callback=self._streaming_agent_response_callback, ), ) logger.debug(f"Registered agent actor of type {self._get_agent_actor_type(agent, internal_topic_type)}") next_actor_type = self._get_agent_actor_type(agent, internal_topic_type) async def _register_collection_actor( self, runtime: CoreRuntime, internal_topic_type: str, exception_callback: Callable[[BaseException], None], result_callback: Callable[[DefaultTypeAlias], Awaitable[None]], ) -> None: """Register the collection actor.""" await CollectionActor.register( runtime, self._get_collection_actor_type(internal_topic_type), lambda: CollectionActor( description="An internal agent that is responsible for collection results", exception_callback=exception_callback, result_callback=result_callback, ), ) def _get_agent_actor_type(self, agent: Agent, internal_topic_type: str) -> str: """Get the actor type for an agent. The type is appended with the internal topic type to ensure uniqueness in the runtime that may be shared by multiple orchestrations. """ return f"{agent.name}_{internal_topic_type}" def _get_collection_actor_type(self, internal_topic_type: str) -> str: """Get the collection actor type. The type is appended with the internal topic type to ensure uniqueness in the runtime that may be shared by multiple orchestrations. """ return f"{CollectionActor.__name__}_{internal_topic_type}"