# Copyright (c) Microsoft. All rights reserved. import asyncio import logging import sys from abc import ABC, abstractmethod from collections.abc import Awaitable, Callable from html import escape from typing import Annotated from pydantic import Field 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.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.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.agents.runtime.core.topic import TopicId from semantic_kernel.agents.runtime.in_process.type_subscription import TypeSubscription 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 semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.kernel import Kernel from semantic_kernel.kernel_pydantic import KernelBaseModel from semantic_kernel.prompt_template.kernel_prompt_template import KernelPromptTemplate from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig 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__) # region Messages and Types @experimental class MagenticStartMessage(KernelBaseModel): """A message to start a magentic group chat.""" body: ChatMessageContent @experimental class MagenticRequestMessage(KernelBaseModel): """A request message type for agents in a magentic group chat.""" agent_name: str @experimental class MagenticResponseMessage(KernelBaseModel): """A response message type from agents in a magentic group chat.""" body: ChatMessageContent @experimental class MagenticResetMessage(KernelBaseModel): """A message to reset a participant's chat history in a magentic group chat.""" pass @experimental class ProgressLedgerItem(KernelBaseModel): """A progress ledger item.""" reason: str answer: str | bool @experimental class ProgressLedger(KernelBaseModel): """A progress ledger.""" is_request_satisfied: ProgressLedgerItem is_in_loop: ProgressLedgerItem is_progress_being_made: ProgressLedgerItem next_speaker: ProgressLedgerItem instruction_or_question: ProgressLedgerItem @experimental class MagenticContext(KernelBaseModel): """Context for the Magentic manager.""" task: Annotated[ChatMessageContent, Field(description="The task to be completed.")] chat_history: Annotated[ ChatHistory, Field(description="The chat history to be used to generate the facts and plan.") ] = ChatHistory() participant_descriptions: Annotated[ dict[str, str], Field(description="The descriptions of the participants in the group.") ] round_count: Annotated[int, Field(description="The number of rounds completed.")] = 0 stall_count: Annotated[int, Field(description="The number of stalls detected.")] = 0 reset_count: Annotated[int, Field(description="The number of resets detected.")] = 0 def reset(self) -> None: """Reset the context. This will clear the chat history and reset the stall count. This won't reset the task, round count, or participant descriptions. """ self.chat_history.clear() self.stall_count = 0 self.reset_count += 1 # endregion Messages and Types # region MagenticManager @experimental class MagenticManagerBase(KernelBaseModel, ABC): """Base class for the Magentic One manager.""" max_stall_count: Annotated[int, Field(description="The maximum number of stalls allowed before a reset.", ge=0)] = 3 max_reset_count: Annotated[int | None, Field(description="The maximum number of resets allowed.", ge=0)] = None max_round_count: Annotated[ int | None, Field(description="The maximum number of rounds (agent responses) allowed.", gt=0) ] = None @abstractmethod async def plan(self, magentic_context: MagenticContext) -> ChatMessageContent: """Create a plan for the task. This is called when the task is first started. Args: magentic_context (MagenticContext): The context for the Magentic manager. Returns: ChatMessageContent: The task ledger. """ ... @abstractmethod async def replan(self, magentic_context: MagenticContext) -> ChatMessageContent: """Replan for the task. This is called when the task is stalled or looping. Args: magentic_context (MagenticContext): The context for the Magentic manager. Returns: ChatMessageContent: The updated task ledger. """ ... @abstractmethod async def create_progress_ledger(self, magentic_context: MagenticContext) -> ProgressLedger: """Create a progress ledger. Args: magentic_context (MagenticContext): The context for the Magentic manager. Returns: ProgressLedger: The progress ledger. """ ... @abstractmethod async def prepare_final_answer(self, magentic_context: MagenticContext) -> ChatMessageContent: """Prepare the final answer. Args: magentic_context (MagenticContext): The context for the Magentic manager. Returns: ChatMessageContent: The final answer. """ ... @experimental class _TaskLedger(KernelBaseModel): """Task ledger for the Standard Magentic manager.""" facts: Annotated[ChatMessageContent, Field(description="The facts about the task.")] plan: Annotated[ChatMessageContent, Field(description="The plan for the task.")] @experimental class StandardMagenticManager(MagenticManagerBase): """Standard Magentic manager implementation. This is the default implementation of the Magentic manager. It uses the task ledger to keep track of the facts and plan for the task. This implementation requires a chat completion model with structured outputs. """ chat_completion_service: ChatCompletionClientBase prompt_execution_settings: PromptExecutionSettings task_ledger_facts_prompt: str = ORCHESTRATOR_TASK_LEDGER_FACTS_PROMPT task_ledger_plan_prompt: str = ORCHESTRATOR_TASK_LEDGER_PLAN_PROMPT task_ledger_full_prompt: str = ORCHESTRATOR_TASK_LEDGER_FULL_PROMPT task_ledger_facts_update_prompt: str = ORCHESTRATOR_TASK_LEDGER_FACTS_UPDATE_PROMPT task_ledger_plan_update_prompt: str = ORCHESTRATOR_TASK_LEDGER_PLAN_UPDATE_PROMPT progress_ledger_prompt: str = ORCHESTRATOR_PROGRESS_LEDGER_PROMPT final_answer_prompt: str = ORCHESTRATOR_FINAL_ANSWER_PROMPT task_ledger: _TaskLedger | None = None def __init__( self, chat_completion_service: ChatCompletionClientBase, prompt_execution_settings: PromptExecutionSettings | None = None, **kwargs, ) -> None: """Initialize the Standard Magentic manager. Args: chat_completion_service (ChatCompletionClientBase): The chat completion service to use. prompt_execution_settings (PromptExecutionSettings | None): The prompt execution settings to use. **kwargs: Additional keyword arguments for prompts: - task_ledger_facts_prompt: The prompt to use for the task ledger facts. - task_ledger_plan_prompt: The prompt to use for the task ledger plan. - task_ledger_full_prompt: The prompt to use for the full task ledger. - task_ledger_facts_update_prompt: The prompt to use for the task ledger facts update. - task_ledger_plan_update_prompt: The prompt to use for the task ledger plan update. - progress_ledger_prompt: The prompt to use for the progress ledger. - final_answer_prompt: The prompt to use for the final answer. """ # Bast effort to make sure the service supports structured output. Even if the service supports # structured output, the model may not support it, in which case there is no good way to check. if prompt_execution_settings is None: prompt_execution_settings = chat_completion_service.instantiate_prompt_execution_settings() if not hasattr(prompt_execution_settings, "response_format"): raise ValueError("The service must support structured output.") else: if not hasattr(prompt_execution_settings, "response_format"): raise ValueError("The service must support structured output.") if getattr(prompt_execution_settings, "response_format", None) is not None: raise ValueError("The prompt execution settings must not have a response format set.") super().__init__( chat_completion_service=chat_completion_service, prompt_execution_settings=prompt_execution_settings, **kwargs, ) @override async def plan(self, magentic_context: MagenticContext) -> ChatMessageContent: """Plan the task. Args: magentic_context (MagenticContext): The context for the Magentic manager. Returns: ChatMessageContent: The task ledger. """ # 1. Gather the facts prompt_template = KernelPromptTemplate( prompt_template_config=PromptTemplateConfig(template=self.task_ledger_facts_prompt) ) magentic_context.chat_history.add_message( ChatMessageContent( role=AuthorRole.USER, content=await prompt_template.render(Kernel(), KernelArguments(task=magentic_context.task.content)), ) ) facts = await self.chat_completion_service.get_chat_message_content( magentic_context.chat_history, self.prompt_execution_settings, ) assert facts is not None # nosec B101 magentic_context.chat_history.add_message(facts) # 2. Create the plan prompt_template = KernelPromptTemplate( prompt_template_config=PromptTemplateConfig(template=self.task_ledger_plan_prompt), allow_dangerously_set_content=True, ) escaped_participant_descriptions: dict[str, str] = {} for key, value in magentic_context.participant_descriptions.items(): escaped_participant_descriptions[key] = escape(value) magentic_context.chat_history.add_message( ChatMessageContent( role=AuthorRole.USER, content=await prompt_template.render( Kernel(), KernelArguments(team=escaped_participant_descriptions), ), ) ) plan = await self.chat_completion_service.get_chat_message_content( magentic_context.chat_history, self.prompt_execution_settings, ) assert plan is not None # nosec B101 self.task_ledger = _TaskLedger(facts=facts, plan=plan) return await self._render_task_ledger(magentic_context) @override async def replan(self, magentic_context: MagenticContext) -> ChatMessageContent: """Replan the task. Args: magentic_context (MagenticContext): The context for the Magentic manager. Returns: ChatMessageContent: The updated task ledger. """ if self.task_ledger is None: raise RuntimeError("The task ledger is not initialized. Planning needs to happen first.") # 1. Update the facts prompt_template = KernelPromptTemplate( prompt_template_config=PromptTemplateConfig(template=self.task_ledger_facts_update_prompt) ) magentic_context.chat_history.add_message( ChatMessageContent( role=AuthorRole.USER, content=await prompt_template.render( Kernel(), KernelArguments(task=magentic_context.task.content, old_facts=self.task_ledger.facts.content), ), ) ) facts = await self.chat_completion_service.get_chat_message_content( magentic_context.chat_history, self.prompt_execution_settings, ) assert facts is not None # nosec B101 magentic_context.chat_history.add_message(facts) # 2. Update the plan prompt_template = KernelPromptTemplate( prompt_template_config=PromptTemplateConfig(template=self.task_ledger_plan_update_prompt), allow_dangerously_set_content=True, ) escaped_participant_descriptions: dict[str, str] = {} for key, value in magentic_context.participant_descriptions.items(): escaped_participant_descriptions[key] = escape(value) magentic_context.chat_history.add_message( ChatMessageContent( role=AuthorRole.USER, content=await prompt_template.render( Kernel(), KernelArguments(team=escaped_participant_descriptions), ), ) ) plan = await self.chat_completion_service.get_chat_message_content( magentic_context.chat_history, self.prompt_execution_settings, ) assert plan is not None # nosec B101 self.task_ledger.facts = facts self.task_ledger.plan = plan return await self._render_task_ledger(magentic_context) async def _render_task_ledger(self, magentic_context: MagenticContext) -> ChatMessageContent: """Render the task ledger to a string. Args: magentic_context (MagenticContext): The context for the Magentic manager. Returns: ChatMessageContent: The rendered task ledger. """ if self.task_ledger is None: raise RuntimeError("The task ledger is not initialized. Planning needs to happen first.") prompt_template = KernelPromptTemplate( prompt_template_config=PromptTemplateConfig(template=self.task_ledger_full_prompt), allow_dangerously_set_content=True, ) escaped_participant_descriptions: dict[str, str] = {} for key, value in magentic_context.participant_descriptions.items(): escaped_participant_descriptions[key] = escape(value) rendered_task_ledger = await prompt_template.render( Kernel(), KernelArguments( task=magentic_context.task.content, team=escaped_participant_descriptions, facts=self.task_ledger.facts.content, plan=self.task_ledger.plan.content, ), ) return ChatMessageContent(role=AuthorRole.ASSISTANT, content=rendered_task_ledger) @override async def create_progress_ledger(self, magentic_context: MagenticContext) -> ProgressLedger: """Create a progress ledger. Args: magentic_context (MagenticContext): The context for the Magentic manager. Returns: ProgressLedger: The progress ledger. """ prompt_template = KernelPromptTemplate( prompt_template_config=PromptTemplateConfig(template=self.progress_ledger_prompt), allow_dangerously_set_content=True, ) escaped_participant_descriptions: dict[str, str] = {} for key, value in magentic_context.participant_descriptions.items(): escaped_participant_descriptions[key] = escape(value) progress_ledger_prompt = await prompt_template.render( Kernel(), KernelArguments( task=magentic_context.task.content, team=escaped_participant_descriptions, names=", ".join(magentic_context.participant_descriptions.keys()), ), ) magentic_context.chat_history.add_message( ChatMessageContent(role=AuthorRole.USER, content=progress_ledger_prompt) ) prompt_execution_settings_clone = PromptExecutionSettings.from_prompt_execution_settings( self.prompt_execution_settings ) prompt_execution_settings_clone.update_from_prompt_execution_settings( PromptExecutionSettings(extension_data={"response_format": ProgressLedger}) ) response = await self.chat_completion_service.get_chat_message_content( magentic_context.chat_history, prompt_execution_settings_clone, ) assert response is not None # nosec B101 return ProgressLedger.model_validate_json(response.content) @override async def prepare_final_answer(self, magentic_context: MagenticContext) -> ChatMessageContent: """Prepare the final answer. Args: magentic_context (MagenticContext): The context for the Magentic manager. Returns: ChatMessageContent: The final answer. """ prompt_template = KernelPromptTemplate( prompt_template_config=PromptTemplateConfig(template=self.final_answer_prompt), allow_dangerously_set_content=True, ) magentic_context.task.content = escape(magentic_context.task.content) magentic_context.chat_history.add_message( ChatMessageContent( role=AuthorRole.USER, content=await prompt_template.render(Kernel(), KernelArguments(task=magentic_context.task)), ) ) response = await self.chat_completion_service.get_chat_message_content( magentic_context.chat_history, self.prompt_execution_settings, ) assert response is not None # nosec B101 return response # endregion MagenticManager # region MagenticManagerActor @experimental class MagenticManagerActor(ActorBase): """Actor for the Magentic One manager.""" def __init__( self, manager: MagenticManagerBase, internal_topic_type: str, participant_descriptions: dict[str, str], exception_callback: Callable[[BaseException], None], result_callback: Callable[[DefaultTypeAlias], Awaitable[None]] | None = None, ) -> None: """Initialize the Magentic One manager actor. Args: manager (MagenticManagerBase): The Magentic One manager. internal_topic_type (str): The internal topic type. participant_descriptions (dict[str, str]): The participant descriptions. exception_callback (Callable[[BaseException], None]): A callback function to handle exceptions. result_callback (Callable | None): A callback function to handle the final answer. """ self._manager = manager self._internal_topic_type = internal_topic_type self._result_callback = result_callback self._participant_descriptions = participant_descriptions self._context: MagenticContext | None = None self._task_ledger: ChatMessageContent | None = None super().__init__("Magentic One Manager", exception_callback) @message_handler @ActorBase.exception_handler async def _handle_start_message(self, message: MagenticStartMessage, ctx: MessageContext) -> None: """Handle the start message for the Magentic One manager.""" logger.debug(f"{self.id}: Received Magentic One start message.") self._context = MagenticContext( task=message.body, participant_descriptions=self._participant_descriptions, ) # Initial planning self._task_ledger = await self._manager.plan(self._context.model_copy(deep=True)) await self._run_outer_loop(ctx.cancellation_token) @message_handler @ActorBase.exception_handler async def _handle_response_message(self, message: MagenticResponseMessage, ctx: MessageContext) -> None: """Handle the response message for the Magentic One manager.""" if self._context is None or self._task_ledger is None: raise RuntimeError("The Magentic manager is not started yet. Make sure to send a start message first.") if message.body.role != AuthorRole.USER: self._context.chat_history.add_message( ChatMessageContent( role=AuthorRole.USER, content=f"Transferred to {message.body.name}", ) ) self._context.chat_history.add_message(message.body) logger.debug(f"{self.id}: Running inner loop.") await self._run_inner_loop(ctx.cancellation_token) async def _run_outer_loop(self, cancellation_token: CancellationToken) -> None: if self._context is None or self._task_ledger is None: raise RuntimeError("The Magentic manager is not started yet. Make sure to send a start message first.") # 1. Publish the rendered task ledger to the group chat. # Need to add the task ledger to the orchestrator's chat history # since the publisher won't receive the message it sends even though # the publisher also subscribes to the topic. self._context.chat_history.add_message( ChatMessageContent( role=AuthorRole.ASSISTANT, content=self._task_ledger.content, name=self.__class__.__name__, ) ) logger.debug(f"Initial task ledger:\n{self._task_ledger.content}") await self.publish_message( MagenticResponseMessage( body=self._context.chat_history.messages[-1], ), TopicId(self._internal_topic_type, self.id.key), cancellation_token=cancellation_token, ) # 2. Start the inner loop. await self._run_inner_loop(cancellation_token) async def _run_inner_loop(self, cancellation_token: CancellationToken) -> None: if self._context is None or self._task_ledger is None: raise RuntimeError("The Magentic manager is not started yet. Make sure to send a start message first.") within_limits = await self._check_within_limits() if not within_limits: return self._context.round_count += 1 # 1. Create a progress ledger current_progress_ledger = await self._manager.create_progress_ledger(self._context.model_copy(deep=True)) logger.debug(f"Current progress ledger:\n{current_progress_ledger.model_dump_json(indent=2)}") # 2. Process the progress ledger # 2.1 Check for task completion if current_progress_ledger.is_request_satisfied.answer: logger.debug("Task completed.") await self._prepare_final_answer() return # 2.2 Check for stalling or looping if not current_progress_ledger.is_progress_being_made.answer or current_progress_ledger.is_in_loop.answer: self._context.stall_count += 1 else: self._context.stall_count = max(0, self._context.stall_count - 1) if self._context.stall_count > self._manager.max_stall_count: logger.debug("Stalling detected. Resetting the task.") self._task_ledger = await self._manager.replan(self._context.model_copy(deep=True)) await self._reset_for_outer_loop(cancellation_token) logger.debug("Restarting outer loop.") await self._run_outer_loop(cancellation_token) return # 2.3 Publish for next step next_step = current_progress_ledger.instruction_or_question.answer self._context.chat_history.add_message( ChatMessageContent( role=AuthorRole.ASSISTANT, content=next_step if isinstance(next_step, str) else str(next_step), name=self.__class__.__name__, ) ) await self.publish_message( MagenticResponseMessage( body=self._context.chat_history.messages[-1], ), TopicId(self._internal_topic_type, self.id.key), cancellation_token=cancellation_token, ) # 2.4 Request the next speaker to speak next_speaker = current_progress_ledger.next_speaker.answer if next_speaker not in self._participant_descriptions: raise ValueError(f"Unknown speaker: {next_speaker}") logger.debug(f"Magentic One manager selected agent: {next_speaker}") await self.publish_message( MagenticRequestMessage(agent_name=next_speaker), TopicId(self._internal_topic_type, self.id.key), cancellation_token=cancellation_token, ) async def _reset_for_outer_loop(self, cancellation_token: CancellationToken) -> None: """Reset the context for the outer loop.""" if self._context is None: raise RuntimeError("The Magentic manager is not started yet. Make sure to send a start message first.") await self.publish_message( MagenticResetMessage(), TopicId(self._internal_topic_type, self.id.key), cancellation_token=cancellation_token, ) self._context.reset() async def _prepare_final_answer(self) -> None: """Prepare the final answer and send it to the result callback.""" if self._context is None: raise RuntimeError("The Magentic manager is not started yet. Make sure to send a start message first.") final_answer = await self._manager.prepare_final_answer(self._context.model_copy(deep=True)) if self._result_callback: await self._result_callback(final_answer) async def _check_within_limits(self) -> bool: """Check if the manager is within the limits.""" if self._context is None: raise RuntimeError("The Magentic manager is not started yet. Make sure to send a start message first.") hit_round_limit = ( self._manager.max_round_count is not None and self._context.round_count >= self._manager.max_round_count ) hit_reset_limit = ( self._manager.max_reset_count is not None and self._context.reset_count > self._manager.max_reset_count ) if hit_round_limit or hit_reset_limit: limit_type = "round" if hit_round_limit else "reset" logger.error(f"Max {limit_type} count reached.") # Retrieve the latest assistant content produced so far partial_result = next( (m for m in reversed(self._context.chat_history.messages) if m.role == AuthorRole.ASSISTANT), None, ) if partial_result is None: partial_result = ChatMessageContent( role=AuthorRole.ASSISTANT, content=f"Stopped because the maximum {limit_type} limit was reached. No partial result available.", name=self.__class__.__name__, ) if self._result_callback: await self._result_callback(partial_result) return False return True # endregion MagenticManagerActor # region MagenticAgentActor @experimental class MagenticAgentActor(AgentActorBase): """An agent actor that process messages in a Magentic One group chat.""" @message_handler async def _handle_response_message(self, message: MagenticResponseMessage, ctx: MessageContext) -> None: logger.debug(f"{self.id}: Received response message.") self._message_cache.add_message(message.body) @message_handler async def _handle_request_message(self, message: MagenticRequestMessage, ctx: MessageContext) -> None: if message.agent_name != self._agent.name: return logger.debug(f"{self.id}: Received request message.") response = await self._invoke_agent() logger.debug(f"{self.id} responded with {response}.") await self.publish_message( MagenticResponseMessage(body=response), TopicId(self._internal_topic_type, self.id.key), cancellation_token=ctx.cancellation_token, ) @message_handler async def _handle_reset_message(self, message: MagenticResetMessage, ctx: MessageContext) -> None: """Handle the reset message for the Magentic One group chat.""" logger.debug(f"{self.id}: Received reset message.") self._message_cache.clear() if self._agent_thread: await self._agent_thread.delete() self._agent_thread = None # endregion MagenticAgentActor # region MagenticOrchestration @experimental class MagenticOrchestration(OrchestrationBase[TIn, TOut]): """The Magentic One pattern orchestration.""" def __init__( self, members: list[Agent], manager: MagenticManagerBase, name: str | None = None, description: str | None = None, input_transform: Callable[[TIn], Awaitable[DefaultTypeAlias] | DefaultTypeAlias] | None = None, output_transform: Callable[[DefaultTypeAlias], Awaitable[TOut] | TOut] | None = 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 Magentic One orchestration. Args: members (list[Agent]): A list of agents. manager (MagenticManagerBase): The manager for the Magentic One pattern. name (str | None): The name of the orchestration. description (str | None): The description of the orchestration. input_transform (Callable | None): A function that transforms the external input message. output_transform (Callable | None): A function that transforms the internal output message. agent_response_callback (Callable | None): A function that is called when a response is produced by the agents. streaming_agent_response_callback (Callable | None): A function that is called when a streaming response is produced by the agents. """ self._manager = manager for member in members: if member.description is None: raise ValueError("All members must have a description.") super().__init__( members=members, name=name, description=description, input_transform=input_transform, output_transform=output_transform, agent_response_callback=agent_response_callback, streaming_agent_response_callback=streaming_agent_response_callback, ) @override async def _start( self, task: DefaultTypeAlias, runtime: CoreRuntime, internal_topic_type: str, cancellation_token: CancellationToken, ) -> None: """Start the Magentic pattern.""" if not isinstance(task, ChatMessageContent): # Magentic One only supports ChatMessageContent as input. raise ValueError("The task must be a ChatMessageContent object.") target_actor_id = await runtime.get(self._get_manager_actor_type(internal_topic_type)) await runtime.send_message( MagenticStartMessage(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_manager(runtime, internal_topic_type, exception_callback, result_callback=result_callback) await self._add_subscriptions(runtime, internal_topic_type) async def _register_members( self, runtime: CoreRuntime, internal_topic_type: str, exception_callback: Callable[[BaseException], None], ) -> None: """Register the agents.""" await asyncio.gather(*[ MagenticAgentActor.register( runtime, self._get_agent_actor_type(agent, internal_topic_type), lambda agent=agent: MagenticAgentActor( # type: ignore[misc] agent, internal_topic_type, exception_callback, self._agent_response_callback, self._streaming_agent_response_callback, ), ) for agent in self._members ]) async def _register_manager( self, runtime: CoreRuntime, internal_topic_type: str, exception_callback: Callable[[BaseException], None], result_callback: Callable[[DefaultTypeAlias], Awaitable[None]] | None = None, ) -> None: """Register the group chat manager.""" await MagenticManagerActor.register( runtime, self._get_manager_actor_type(internal_topic_type), lambda: MagenticManagerActor( self._manager, internal_topic_type=internal_topic_type, participant_descriptions={agent.name: agent.description for agent in self._members}, # type: ignore[misc] exception_callback=exception_callback, result_callback=result_callback, ), ) async def _add_subscriptions(self, runtime: CoreRuntime, internal_topic_type: str) -> None: subscriptions: list[TypeSubscription] = [] for agent in self._members: subscriptions.append( TypeSubscription(internal_topic_type, self._get_agent_actor_type(agent, internal_topic_type)) ) subscriptions.append(TypeSubscription(internal_topic_type, self._get_manager_actor_type(internal_topic_type))) await asyncio.gather(*[runtime.add_subscription(sub) for sub in subscriptions]) 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_manager_actor_type(self, internal_topic_type: str) -> str: """Get the actor type for the group chat manager. The type is appended with the internal topic type to ensure uniqueness in the runtime that may be shared by multiple orchestrations. """ return f"{MagenticManagerActor.__name__}_{internal_topic_type}" # endregion MagenticOrchestration