From 777f2abbd72a3e494b0ebb532ccd478bd47d0b2b Mon Sep 17 00:00:00 2001 From: Victor Dibia Date: Wed, 4 Dec 2024 16:14:41 -0800 Subject: [PATCH] Load and Save state in AgentChat (#4436) 1. convert dataclass types to pydantic basemodel 2. add save_state and load_state for ChatAgent 3. state types for AgentChat --------- Co-authored-by: Eric Zhu --- .../Templates/MagenticOne/scenario.py | 2 +- .../agents/_assistant_agent.py | 20 +- .../agents/_base_chat_agent.py | 11 +- .../src/autogen_agentchat/base/_chat_agent.py | 10 +- .../src/autogen_agentchat/base/_team.py | 10 +- .../src/autogen_agentchat/messages.py | 24 +- .../src/autogen_agentchat/state/__init__.py | 25 + .../src/autogen_agentchat/state/_states.py | 81 ++ .../teams/_group_chat/_base_group_chat.py | 38 +- .../_group_chat/_chat_agent_container.py | 13 +- .../_magentic_one_orchestrator.py | 25 +- .../_group_chat/_round_robin_group_chat.py | 17 +- .../teams/_group_chat/_selector_group_chat.py | 22 +- .../teams/_group_chat/_swarm_group_chat.py | 21 +- .../tests/test_assistant_agent.py | 17 +- .../tests/test_group_chat.py | 95 +- .../tests/test_magentic_one_group_chat.py | 24 +- .../agentchat-user-guide/tutorial/index.md | 7 + .../agentchat-user-guide/tutorial/state.ipynb | 299 ++++ .../cookbook/tool-use-with-intervention.ipynb | 562 ++++---- .../topic-subscription-scenarios.ipynb | 1198 ++++++++-------- .../design-patterns/group-chat.ipynb | 4 +- .../design-patterns/mixture-of-agents.ipynb | 1036 +++++++------- .../design-patterns/multi-agent-debate.ipynb | 1138 +++++++-------- .../framework/model-clients.ipynb | 1238 ++++++++--------- .../core-user-guide/framework/tools.ipynb | 626 ++++----- .../common/patterns/_group_chat_utils.py | 2 +- .../autogen-core/samples/common/utils.py | 2 +- .../samples/distributed-group-chat/_agents.py | 4 +- .../samples/slow_human_in_loop.py | 6 +- .../src/autogen_core/_closure_agent.py | 6 +- .../autogen_core/components/models/_types.py | 35 +- .../agents/file_surfer/_file_surfer.py | 8 +- .../web_surfer/_multimodal_web_surfer.py | 4 +- .../test_reply_chat_completion_client.py | 2 +- .../src/autogen_magentic_one/agents/coder.py | 6 +- .../agents/file_surfer/file_surfer.py | 6 +- .../multimodal_web_surfer.py | 2 +- .../agents/orchestrator.py | 2 +- 39 files changed, 3684 insertions(+), 2964 deletions(-) create mode 100644 python/packages/autogen-agentchat/src/autogen_agentchat/state/__init__.py create mode 100644 python/packages/autogen-agentchat/src/autogen_agentchat/state/_states.py create mode 100644 python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/state.ipynb diff --git a/python/packages/agbench/benchmarks/WebArena/Templates/MagenticOne/scenario.py b/python/packages/agbench/benchmarks/WebArena/Templates/MagenticOne/scenario.py index 306c87826bc4..22a6818513f8 100644 --- a/python/packages/agbench/benchmarks/WebArena/Templates/MagenticOne/scenario.py +++ b/python/packages/agbench/benchmarks/WebArena/Templates/MagenticOne/scenario.py @@ -127,7 +127,7 @@ async def main() -> None: lambda: Coder( model_client=client, system_messages=[ - SystemMessage("""You are a general-purpose AI assistant and can handle many questions -- but you don't have access to a web browser. However, the user you are talking to does have a browser, and you can see the screen. Provide short direct instructions to them to take you where you need to go to answer the initial question posed to you. + SystemMessage(content="""You are a general-purpose AI assistant and can handle many questions -- but you don't have access to a web browser. However, the user you are talking to does have a browser, and you can see the screen. Provide short direct instructions to them to take you where you need to go to answer the initial question posed to you. Once the user has taken the final necessary action to complete the task, and you have fully addressed the initial request, reply with the word TERMINATE.""", ) diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_assistant_agent.py b/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_assistant_agent.py index de2087382254..23f62694dd62 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_assistant_agent.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_assistant_agent.py @@ -2,7 +2,7 @@ import json import logging import warnings -from typing import Any, AsyncGenerator, Awaitable, Callable, Dict, List, Sequence +from typing import Any, AsyncGenerator, Awaitable, Callable, Dict, List, Mapping, Sequence from autogen_core import CancellationToken, FunctionCall from autogen_core.components.models import ( @@ -29,6 +29,7 @@ ToolCallMessage, ToolCallResultMessage, ) +from ..state import AssistantAgentState from ._base_chat_agent import BaseChatAgent event_logger = logging.getLogger(EVENT_LOGGER_NAME) @@ -49,6 +50,12 @@ def model_post_init(self, __context: Any) -> None: class AssistantAgent(BaseChatAgent): """An agent that provides assistance with tool use. + ```{note} + The assistant agent is not thread-safe or coroutine-safe. + It should not be shared between multiple tasks or coroutines, and it should + not call its methods concurrently. + ``` + Args: name (str): The name of the agent. model_client (ChatCompletionClient): The model client to use for inference. @@ -224,6 +231,7 @@ def __init__( f"Handoff names must be unique from tool names. Handoff names: {handoff_tool_names}; tool names: {tool_names}" ) self._model_context: List[LLMMessage] = [] + self._is_running = False @property def produced_message_types(self) -> List[type[ChatMessage]]: @@ -327,3 +335,13 @@ async def _execute_tool_call( async def on_reset(self, cancellation_token: CancellationToken) -> None: """Reset the assistant agent to its initialization state.""" self._model_context.clear() + + async def save_state(self) -> Mapping[str, Any]: + """Save the current state of the assistant agent.""" + return AssistantAgentState(llm_messages=self._model_context.copy()).model_dump() + + async def load_state(self, state: Mapping[str, Any]) -> None: + """Load the state of the assistant agent""" + assistant_agent_state = AssistantAgentState.model_validate(state) + self._model_context.clear() + self._model_context.extend(assistant_agent_state.llm_messages) diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_base_chat_agent.py b/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_base_chat_agent.py index 01eafa86a411..5b2aed4860c1 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_base_chat_agent.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/agents/_base_chat_agent.py @@ -1,10 +1,11 @@ from abc import ABC, abstractmethod -from typing import AsyncGenerator, List, Sequence +from typing import Any, AsyncGenerator, List, Mapping, Sequence from autogen_core import CancellationToken from ..base import ChatAgent, Response, TaskResult from ..messages import AgentMessage, ChatMessage, HandoffMessage, MultiModalMessage, StopMessage, TextMessage +from ..state import BaseState class BaseChatAgent(ChatAgent, ABC): @@ -117,3 +118,11 @@ async def run_stream( async def on_reset(self, cancellation_token: CancellationToken) -> None: """Resets the agent to its initialization state.""" ... + + async def save_state(self) -> Mapping[str, Any]: + """Export state. Default implementation for stateless agents.""" + return BaseState().model_dump() + + async def load_state(self, state: Mapping[str, Any]) -> None: + """Restore agent from saved state. Default implementation for stateless agents.""" + BaseState.model_validate(state) diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/base/_chat_agent.py b/python/packages/autogen-agentchat/src/autogen_agentchat/base/_chat_agent.py index 861389cb78cd..c27dc232a3d9 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/base/_chat_agent.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/base/_chat_agent.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import AsyncGenerator, List, Protocol, Sequence, runtime_checkable +from typing import Any, AsyncGenerator, List, Mapping, Protocol, Sequence, runtime_checkable from autogen_core import CancellationToken @@ -54,3 +54,11 @@ def on_messages_stream( async def on_reset(self, cancellation_token: CancellationToken) -> None: """Resets the agent to its initialization state.""" ... + + async def save_state(self) -> Mapping[str, Any]: + """Save agent state for later restoration""" + ... + + async def load_state(self, state: Mapping[str, Any]) -> None: + """Restore agent from saved state""" + ... diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/base/_team.py b/python/packages/autogen-agentchat/src/autogen_agentchat/base/_team.py index 198d50179f1f..10999b0500ad 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/base/_team.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/base/_team.py @@ -1,4 +1,4 @@ -from typing import Protocol +from typing import Any, Mapping, Protocol from ._task import TaskRunner @@ -7,3 +7,11 @@ class Team(TaskRunner, Protocol): async def reset(self) -> None: """Reset the team and all its participants to its initial state.""" ... + + async def save_state(self) -> Mapping[str, Any]: + """Save the current state of the team.""" + ... + + async def load_state(self, state: Mapping[str, Any]) -> None: + """Load the state of the team.""" + ... diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/messages.py b/python/packages/autogen-agentchat/src/autogen_agentchat/messages.py index b7650d581a78..fe3bf110d5e6 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/messages.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/messages.py @@ -1,8 +1,9 @@ -from typing import List +from typing import List, Literal from autogen_core import FunctionCall, Image from autogen_core.components.models import FunctionExecutionResult, RequestUsage -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, Field +from typing_extensions import Annotated class BaseMessage(BaseModel): @@ -23,6 +24,8 @@ class TextMessage(BaseMessage): content: str """The content of the message.""" + type: Literal["TextMessage"] = "TextMessage" + class MultiModalMessage(BaseMessage): """A multimodal message.""" @@ -30,6 +33,8 @@ class MultiModalMessage(BaseMessage): content: List[str | Image] """The content of the message.""" + type: Literal["MultiModalMessage"] = "MultiModalMessage" + class StopMessage(BaseMessage): """A message requesting stop of a conversation.""" @@ -37,6 +42,8 @@ class StopMessage(BaseMessage): content: str """The content for the stop message.""" + type: Literal["StopMessage"] = "StopMessage" + class HandoffMessage(BaseMessage): """A message requesting handoff of a conversation to another agent.""" @@ -47,6 +54,8 @@ class HandoffMessage(BaseMessage): content: str """The handoff message to the target agent.""" + type: Literal["HandoffMessage"] = "HandoffMessage" + class ToolCallMessage(BaseMessage): """A message signaling the use of tools.""" @@ -54,6 +63,8 @@ class ToolCallMessage(BaseMessage): content: List[FunctionCall] """The tool calls.""" + type: Literal["ToolCallMessage"] = "ToolCallMessage" + class ToolCallResultMessage(BaseMessage): """A message signaling the results of tool calls.""" @@ -61,12 +72,17 @@ class ToolCallResultMessage(BaseMessage): content: List[FunctionExecutionResult] """The tool call results.""" + type: Literal["ToolCallResultMessage"] = "ToolCallResultMessage" + -ChatMessage = TextMessage | MultiModalMessage | StopMessage | HandoffMessage +ChatMessage = Annotated[TextMessage | MultiModalMessage | StopMessage | HandoffMessage, Field(discriminator="type")] """Messages for agent-to-agent communication.""" -AgentMessage = TextMessage | MultiModalMessage | StopMessage | HandoffMessage | ToolCallMessage | ToolCallResultMessage +AgentMessage = Annotated[ + TextMessage | MultiModalMessage | StopMessage | HandoffMessage | ToolCallMessage | ToolCallResultMessage, + Field(discriminator="type"), +] """All message types.""" diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/state/__init__.py b/python/packages/autogen-agentchat/src/autogen_agentchat/state/__init__.py new file mode 100644 index 000000000000..abb468a70b62 --- /dev/null +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/state/__init__.py @@ -0,0 +1,25 @@ +"""State management for agents, teams and termination conditions.""" + +from ._states import ( + AssistantAgentState, + BaseGroupChatManagerState, + BaseState, + ChatAgentContainerState, + MagenticOneOrchestratorState, + RoundRobinManagerState, + SelectorManagerState, + SwarmManagerState, + TeamState, +) + +__all__ = [ + "BaseState", + "AssistantAgentState", + "BaseGroupChatManagerState", + "ChatAgentContainerState", + "RoundRobinManagerState", + "SelectorManagerState", + "SwarmManagerState", + "MagenticOneOrchestratorState", + "TeamState", +] diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/state/_states.py b/python/packages/autogen-agentchat/src/autogen_agentchat/state/_states.py new file mode 100644 index 000000000000..b266572dd829 --- /dev/null +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/state/_states.py @@ -0,0 +1,81 @@ +from typing import Any, List, Mapping, Optional + +from autogen_core.components.models import ( + LLMMessage, +) +from pydantic import BaseModel, Field + +from ..messages import ( + AgentMessage, + ChatMessage, +) + + +class BaseState(BaseModel): + """Base class for all saveable state""" + + type: str = Field(default="BaseState") + version: str = Field(default="1.0.0") + + +class AssistantAgentState(BaseState): + """State for an assistant agent.""" + + llm_messages: List[LLMMessage] = Field(default_factory=list) + type: str = Field(default="AssistantAgentState") + + +class TeamState(BaseState): + """State for a team of agents.""" + + agent_states: Mapping[str, Any] = Field(default_factory=dict) + team_id: str = Field(default="") + type: str = Field(default="TeamState") + + +class BaseGroupChatManagerState(BaseState): + """Base state for all group chat managers.""" + + message_thread: List[AgentMessage] = Field(default_factory=list) + current_turn: int = Field(default=0) + type: str = Field(default="BaseGroupChatManagerState") + + +class ChatAgentContainerState(BaseState): + """State for a container of chat agents.""" + + agent_state: Mapping[str, Any] = Field(default_factory=dict) + message_buffer: List[ChatMessage] = Field(default_factory=list) + type: str = Field(default="ChatAgentContainerState") + + +class RoundRobinManagerState(BaseGroupChatManagerState): + """State for :class:`~autogen_agentchat.teams.RoundRobinGroupChat` manager.""" + + next_speaker_index: int = Field(default=0) + type: str = Field(default="RoundRobinManagerState") + + +class SelectorManagerState(BaseGroupChatManagerState): + """State for :class:`~autogen_agentchat.teams.SelectorGroupChat` manager.""" + + previous_speaker: Optional[str] = Field(default=None) + type: str = Field(default="SelectorManagerState") + + +class SwarmManagerState(BaseGroupChatManagerState): + """State for :class:`~autogen_agentchat.teams.Swarm` manager.""" + + current_speaker: str = Field(default="") + type: str = Field(default="SwarmManagerState") + + +class MagenticOneOrchestratorState(BaseGroupChatManagerState): + """State for :class:`~autogen_agentchat.teams.MagneticOneGroupChat` orchestrator.""" + + task: str = Field(default="") + facts: str = Field(default="") + plan: str = Field(default="") + n_rounds: int = Field(default=0) + n_stalls: int = Field(default=0) + type: str = Field(default="MagenticOneOrchestratorState") diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_base_group_chat.py b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_base_group_chat.py index fdb79b1197f3..5a798d2b0d55 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_base_group_chat.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_base_group_chat.py @@ -2,7 +2,7 @@ import logging import uuid from abc import ABC, abstractmethod -from typing import AsyncGenerator, Callable, List +from typing import Any, AsyncGenerator, Callable, List, Mapping from autogen_core import ( AgentId, @@ -20,6 +20,7 @@ from ... import EVENT_LOGGER_NAME from ...base import ChatAgent, TaskResult, Team, TerminationCondition from ...messages import AgentMessage, ChatMessage, HandoffMessage, MultiModalMessage, StopMessage, TextMessage +from ...state import TeamState from ._chat_agent_container import ChatAgentContainer from ._events import GroupChatMessage, GroupChatReset, GroupChatStart, GroupChatTermination from ._sequential_routed_agent import SequentialRoutedAgent @@ -493,3 +494,38 @@ async def main() -> None: # Indicate that the team is no longer running. self._is_running = False + + async def save_state(self) -> Mapping[str, Any]: + """Save the state of the group chat team.""" + if not self._initialized: + raise RuntimeError("The group chat has not been initialized. It must be run before it can be saved.") + + if self._is_running: + raise RuntimeError("The team cannot be saved while it is running.") + self._is_running = True + + try: + # Save the state of the runtime. This will save the state of the participants and the group chat manager. + agent_states = await self._runtime.save_state() + return TeamState(agent_states=agent_states, team_id=self._team_id).model_dump() + finally: + # Indicate that the team is no longer running. + self._is_running = False + + async def load_state(self, state: Mapping[str, Any]) -> None: + """Load the state of the group chat team.""" + if not self._initialized: + await self._init(self._runtime) + + if self._is_running: + raise RuntimeError("The team cannot be loaded while it is running.") + self._is_running = True + + try: + # Load the state of the runtime. This will load the state of the participants and the group chat manager. + team_state = TeamState.model_validate(state) + self._team_id = team_state.team_id + await self._runtime.load_state(team_state.agent_states) + finally: + # Indicate that the team is no longer running. + self._is_running = False diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_chat_agent_container.py b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_chat_agent_container.py index 01638def8f05..fdf5428b3b5c 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_chat_agent_container.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_chat_agent_container.py @@ -1,9 +1,10 @@ -from typing import Any, List +from typing import Any, List, Mapping from autogen_core import DefaultTopicId, MessageContext, event, rpc from ...base import ChatAgent, Response from ...messages import ChatMessage +from ...state import ChatAgentContainerState from ._events import GroupChatAgentResponse, GroupChatMessage, GroupChatRequestPublish, GroupChatReset, GroupChatStart from ._sequential_routed_agent import SequentialRoutedAgent @@ -75,3 +76,13 @@ async def handle_request(self, message: GroupChatRequestPublish, ctx: MessageCon async def on_unhandled_message(self, message: Any, ctx: MessageContext) -> None: raise ValueError(f"Unhandled message in agent container: {type(message)}") + + async def save_state(self) -> Mapping[str, Any]: + agent_state = await self._agent.save_state() + state = ChatAgentContainerState(agent_state=agent_state, message_buffer=list(self._message_buffer)) + return state.model_dump() + + async def load_state(self, state: Mapping[str, Any]) -> None: + container_state = ChatAgentContainerState.model_validate(state) + self._message_buffer = list(container_state.message_buffer) + await self._agent.load_state(container_state.agent_state) diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_magentic_one/_magentic_one_orchestrator.py b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_magentic_one/_magentic_one_orchestrator.py index 4758ddd8d8fa..11b143e28c0b 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_magentic_one/_magentic_one_orchestrator.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_magentic_one/_magentic_one_orchestrator.py @@ -1,6 +1,6 @@ import json import logging -from typing import Any, Dict, List +from typing import Any, Dict, List, Mapping from autogen_core import AgentId, CancellationToken, DefaultTopicId, Image, MessageContext, event, rpc from autogen_core.components.models import ( @@ -13,6 +13,7 @@ from .... import TRACE_LOGGER_NAME from ....base import Response, TerminationCondition from ....messages import AgentMessage, ChatMessage, MultiModalMessage, StopMessage, TextMessage +from ....state import MagenticOneOrchestratorState from .._base_group_chat_manager import BaseGroupChatManager from .._events import ( GroupChatAgentResponse, @@ -178,6 +179,28 @@ async def handle_agent_response(self, message: GroupChatAgentResponse, ctx: Mess async def validate_group_state(self, message: ChatMessage | None) -> None: pass + async def save_state(self) -> Mapping[str, Any]: + state = MagenticOneOrchestratorState( + message_thread=list(self._message_thread), + current_turn=self._current_turn, + task=self._task, + facts=self._facts, + plan=self._plan, + n_rounds=self._n_rounds, + n_stalls=self._n_stalls, + ) + return state.model_dump() + + async def load_state(self, state: Mapping[str, Any]) -> None: + orchestrator_state = MagenticOneOrchestratorState.model_validate(state) + self._message_thread = orchestrator_state.message_thread + self._current_turn = orchestrator_state.current_turn + self._task = orchestrator_state.task + self._facts = orchestrator_state.facts + self._plan = orchestrator_state.plan + self._n_rounds = orchestrator_state.n_rounds + self._n_stalls = orchestrator_state.n_stalls + async def select_speaker(self, thread: List[AgentMessage]) -> str: """Not used in this orchestrator, we select next speaker in _orchestrate_step.""" return "" diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_round_robin_group_chat.py b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_round_robin_group_chat.py index 4566bf8fe4db..9ac06ac0b319 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_round_robin_group_chat.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_round_robin_group_chat.py @@ -1,7 +1,8 @@ -from typing import Callable, List +from typing import Any, Callable, List, Mapping from ...base import ChatAgent, TerminationCondition from ...messages import AgentMessage, ChatMessage +from ...state import RoundRobinManagerState from ._base_group_chat import BaseGroupChat from ._base_group_chat_manager import BaseGroupChatManager @@ -38,6 +39,20 @@ async def reset(self) -> None: await self._termination_condition.reset() self._next_speaker_index = 0 + async def save_state(self) -> Mapping[str, Any]: + state = RoundRobinManagerState( + message_thread=list(self._message_thread), + current_turn=self._current_turn, + next_speaker_index=self._next_speaker_index, + ) + return state.model_dump() + + async def load_state(self, state: Mapping[str, Any]) -> None: + round_robin_state = RoundRobinManagerState.model_validate(state) + self._message_thread = list(round_robin_state.message_thread) + self._current_turn = round_robin_state.current_turn + self._next_speaker_index = round_robin_state.next_speaker_index + async def select_speaker(self, thread: List[AgentMessage]) -> str: """Select a speaker from the participants in a round-robin fashion.""" current_speaker_index = self._next_speaker_index diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_selector_group_chat.py b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_selector_group_chat.py index 76d9108c0513..29a3013858ab 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_selector_group_chat.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_selector_group_chat.py @@ -1,10 +1,10 @@ import logging import re -from typing import Callable, Dict, List, Sequence +from typing import Any, Callable, Dict, List, Mapping, Sequence from autogen_core.components.models import ChatCompletionClient, SystemMessage -from ... import EVENT_LOGGER_NAME, TRACE_LOGGER_NAME +from ... import TRACE_LOGGER_NAME from ...base import ChatAgent, TerminationCondition from ...messages import ( AgentMessage, @@ -16,11 +16,11 @@ ToolCallMessage, ToolCallResultMessage, ) +from ...state import SelectorManagerState from ._base_group_chat import BaseGroupChat from ._base_group_chat_manager import BaseGroupChatManager trace_logger = logging.getLogger(TRACE_LOGGER_NAME) -event_logger = logging.getLogger(EVENT_LOGGER_NAME) class SelectorGroupChatManager(BaseGroupChatManager): @@ -64,6 +64,20 @@ async def reset(self) -> None: await self._termination_condition.reset() self._previous_speaker = None + async def save_state(self) -> Mapping[str, Any]: + state = SelectorManagerState( + message_thread=list(self._message_thread), + current_turn=self._current_turn, + previous_speaker=self._previous_speaker, + ) + return state.model_dump() + + async def load_state(self, state: Mapping[str, Any]) -> None: + selector_state = SelectorManagerState.model_validate(state) + self._message_thread = list(selector_state.message_thread) + self._current_turn = selector_state.current_turn + self._previous_speaker = selector_state.previous_speaker + async def select_speaker(self, thread: List[AgentMessage]) -> str: """Selects the next speaker in a group chat using a ChatCompletion client, with the selector function as override if it returns a speaker name. @@ -121,7 +135,7 @@ async def select_speaker(self, thread: List[AgentMessage]) -> str: select_speaker_prompt = self._selector_prompt.format( roles=roles, participants=str(participants), history=history ) - select_speaker_messages = [SystemMessage(select_speaker_prompt)] + select_speaker_messages = [SystemMessage(content=select_speaker_prompt)] response = await self._model_client.create(messages=select_speaker_messages) assert isinstance(response.content, str) mentions = self._mentioned_agents(response.content, self._participant_topic_types) diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_swarm_group_chat.py b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_swarm_group_chat.py index 7feaf3736c72..10574e0a9fa6 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_swarm_group_chat.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_swarm_group_chat.py @@ -1,14 +1,11 @@ -import logging -from typing import Callable, List +from typing import Any, Callable, List, Mapping -from ... import EVENT_LOGGER_NAME from ...base import ChatAgent, TerminationCondition from ...messages import AgentMessage, ChatMessage, HandoffMessage +from ...state import SwarmManagerState from ._base_group_chat import BaseGroupChat from ._base_group_chat_manager import BaseGroupChatManager -event_logger = logging.getLogger(EVENT_LOGGER_NAME) - class SwarmGroupChatManager(BaseGroupChatManager): """A group chat manager that selects the next speaker based on handoff message only.""" @@ -77,6 +74,20 @@ async def select_speaker(self, thread: List[AgentMessage]) -> str: return self._current_speaker return self._current_speaker + async def save_state(self) -> Mapping[str, Any]: + state = SwarmManagerState( + message_thread=list(self._message_thread), + current_turn=self._current_turn, + current_speaker=self._current_speaker, + ) + return state.model_dump() + + async def load_state(self, state: Mapping[str, Any]) -> None: + swarm_state = SwarmManagerState.model_validate(state) + self._message_thread = list(swarm_state.message_thread) + self._current_turn = swarm_state.current_turn + self._current_speaker = swarm_state.current_speaker + class Swarm(BaseGroupChat): """A group chat team that selects the next speaker based on handoff message only. diff --git a/python/packages/autogen-agentchat/tests/test_assistant_agent.py b/python/packages/autogen-agentchat/tests/test_assistant_agent.py index b68ef70cce8b..c09a92a28d3d 100644 --- a/python/packages/autogen-agentchat/tests/test_assistant_agent.py +++ b/python/packages/autogen-agentchat/tests/test_assistant_agent.py @@ -112,12 +112,12 @@ async def test_run_with_tools(monkeypatch: pytest.MonkeyPatch) -> None: ] mock = _MockChatCompletion(chat_completions) monkeypatch.setattr(AsyncCompletions, "create", mock.mock_create) - tool_use_agent = AssistantAgent( + agent = AssistantAgent( "tool_use_agent", model_client=OpenAIChatCompletionClient(model=model, api_key=""), tools=[_pass_function, _fail_function, FunctionTool(_echo_function, description="Echo")], ) - result = await tool_use_agent.run(task="task") + result = await agent.run(task="task") assert len(result.messages) == 4 assert isinstance(result.messages[0], TextMessage) assert result.messages[0].models_usage is None @@ -135,13 +135,24 @@ async def test_run_with_tools(monkeypatch: pytest.MonkeyPatch) -> None: # Test streaming. mock._curr_index = 0 # pyright: ignore index = 0 - async for message in tool_use_agent.run_stream(task="task"): + async for message in agent.run_stream(task="task"): if isinstance(message, TaskResult): assert message == result else: assert message == result.messages[index] index += 1 + # Test state saving and loading. + state = await agent.save_state() + agent2 = AssistantAgent( + "tool_use_agent", + model_client=OpenAIChatCompletionClient(model=model, api_key=""), + tools=[_pass_function, _fail_function, FunctionTool(_echo_function, description="Echo")], + ) + await agent2.load_state(state) + state2 = await agent2.save_state() + assert state == state2 + @pytest.mark.asyncio async def test_handoffs(monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/python/packages/autogen-agentchat/tests/test_group_chat.py b/python/packages/autogen-agentchat/tests/test_group_chat.py index 5c1d681fca07..12115664eb82 100644 --- a/python/packages/autogen-agentchat/tests/test_group_chat.py +++ b/python/packages/autogen-agentchat/tests/test_group_chat.py @@ -28,12 +28,15 @@ SelectorGroupChat, Swarm, ) +from autogen_agentchat.teams._group_chat._round_robin_group_chat import RoundRobinGroupChatManager +from autogen_agentchat.teams._group_chat._selector_group_chat import SelectorGroupChatManager +from autogen_agentchat.teams._group_chat._swarm_group_chat import SwarmGroupChatManager from autogen_agentchat.ui import Console -from autogen_core import CancellationToken, FunctionCall +from autogen_core import AgentId, CancellationToken, FunctionCall from autogen_core.components.code_executor import LocalCommandLineCodeExecutor from autogen_core.components.models import FunctionExecutionResult from autogen_core.components.tools import FunctionTool -from autogen_ext.models import OpenAIChatCompletionClient +from autogen_ext.models import OpenAIChatCompletionClient, ReplayChatCompletionClient from openai.resources.chat.completions import AsyncCompletions from openai.types.chat.chat_completion import ChatCompletion, Choice from openai.types.chat.chat_completion_chunk import ChatCompletionChunk @@ -217,6 +220,38 @@ async def test_round_robin_group_chat(monkeypatch: pytest.MonkeyPatch) -> None: assert result.messages[1:] == result_2.messages[1:] +@pytest.mark.asyncio +async def test_round_robin_group_chat_state() -> None: + model_client = ReplayChatCompletionClient( + ["No facts", "No plan", "print('Hello, world!')", "TERMINATE"], + ) + agent1 = AssistantAgent("agent1", model_client=model_client) + agent2 = AssistantAgent("agent2", model_client=model_client) + termination = TextMentionTermination("TERMINATE") + team1 = RoundRobinGroupChat(participants=[agent1, agent2], termination_condition=termination) + await team1.run(task="Write a program that prints 'Hello, world!'") + state = await team1.save_state() + + agent3 = AssistantAgent("agent1", model_client=model_client) + agent4 = AssistantAgent("agent2", model_client=model_client) + team2 = RoundRobinGroupChat(participants=[agent3, agent4], termination_condition=termination) + await team2.load_state(state) + state2 = await team2.save_state() + assert state == state2 + assert agent3._model_context == agent1._model_context # pyright: ignore + assert agent4._model_context == agent2._model_context # pyright: ignore + manager_1 = await team1._runtime.try_get_underlying_agent_instance( # pyright: ignore + AgentId("group_chat_manager", team1._team_id), # pyright: ignore + RoundRobinGroupChatManager, # pyright: ignore + ) # pyright: ignore + manager_2 = await team2._runtime.try_get_underlying_agent_instance( # pyright: ignore + AgentId("group_chat_manager", team2._team_id), # pyright: ignore + RoundRobinGroupChatManager, # pyright: ignore + ) # pyright: ignore + assert manager_1._current_turn == manager_2._current_turn # pyright: ignore + assert manager_1._message_thread == manager_2._message_thread # pyright: ignore + + @pytest.mark.asyncio async def test_round_robin_group_chat_with_tools(monkeypatch: pytest.MonkeyPatch) -> None: model = "gpt-4o-2024-05-13" @@ -528,6 +563,42 @@ async def test_selector_group_chat(monkeypatch: pytest.MonkeyPatch) -> None: assert result2 == result +@pytest.mark.asyncio +async def test_selector_group_chat_state() -> None: + model_client = ReplayChatCompletionClient( + ["agent1", "No facts", "agent2", "No plan", "agent1", "print('Hello, world!')", "agent2", "TERMINATE"], + ) + agent1 = AssistantAgent("agent1", model_client=model_client) + agent2 = AssistantAgent("agent2", model_client=model_client) + termination = TextMentionTermination("TERMINATE") + team1 = SelectorGroupChat( + participants=[agent1, agent2], termination_condition=termination, model_client=model_client + ) + await team1.run(task="Write a program that prints 'Hello, world!'") + state = await team1.save_state() + + agent3 = AssistantAgent("agent1", model_client=model_client) + agent4 = AssistantAgent("agent2", model_client=model_client) + team2 = SelectorGroupChat( + participants=[agent3, agent4], termination_condition=termination, model_client=model_client + ) + await team2.load_state(state) + state2 = await team2.save_state() + assert state == state2 + assert agent3._model_context == agent1._model_context # pyright: ignore + assert agent4._model_context == agent2._model_context # pyright: ignore + manager_1 = await team1._runtime.try_get_underlying_agent_instance( # pyright: ignore + AgentId("group_chat_manager", team1._team_id), # pyright: ignore + SelectorGroupChatManager, # pyright: ignore + ) # pyright: ignore + manager_2 = await team2._runtime.try_get_underlying_agent_instance( # pyright: ignore + AgentId("group_chat_manager", team2._team_id), # pyright: ignore + SelectorGroupChatManager, # pyright: ignore + ) # pyright: ignore + assert manager_1._message_thread == manager_2._message_thread # pyright: ignore + assert manager_1._previous_speaker == manager_2._previous_speaker # pyright: ignore + + @pytest.mark.asyncio async def test_selector_group_chat_two_speakers(monkeypatch: pytest.MonkeyPatch) -> None: model = "gpt-4o-2024-05-13" @@ -768,6 +839,26 @@ async def test_swarm_handoff() -> None: assert message == result.messages[index] index += 1 + # Test save and load. + state = await team.save_state() + first_agent2 = _HandOffAgent("first_agent", description="first agent", next_agent="second_agent") + second_agent2 = _HandOffAgent("second_agent", description="second agent", next_agent="third_agent") + third_agent2 = _HandOffAgent("third_agent", description="third agent", next_agent="first_agent") + team2 = Swarm([second_agent2, first_agent2, third_agent2], termination_condition=termination) + await team2.load_state(state) + state2 = await team2.save_state() + assert state == state2 + manager_1 = await team._runtime.try_get_underlying_agent_instance( # pyright: ignore + AgentId("group_chat_manager", team._team_id), # pyright: ignore + SwarmGroupChatManager, # pyright: ignore + ) # pyright: ignore + manager_2 = await team2._runtime.try_get_underlying_agent_instance( # pyright: ignore + AgentId("group_chat_manager", team2._team_id), # pyright: ignore + SwarmGroupChatManager, # pyright: ignore + ) # pyright: ignore + assert manager_1._message_thread == manager_2._message_thread # pyright: ignore + assert manager_1._current_speaker == manager_2._current_speaker # pyright: ignore + @pytest.mark.asyncio async def test_swarm_handoff_using_tool_calls(monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/python/packages/autogen-agentchat/tests/test_magentic_one_group_chat.py b/python/packages/autogen-agentchat/tests/test_magentic_one_group_chat.py index 00564928467d..2aa2ba61763f 100644 --- a/python/packages/autogen-agentchat/tests/test_magentic_one_group_chat.py +++ b/python/packages/autogen-agentchat/tests/test_magentic_one_group_chat.py @@ -16,7 +16,8 @@ from autogen_agentchat.teams import ( MagenticOneGroupChat, ) -from autogen_core import CancellationToken +from autogen_agentchat.teams._group_chat._magentic_one._magentic_one_orchestrator import MagenticOneOrchestrator +from autogen_core import AgentId, CancellationToken from autogen_ext.models import ReplayChatCompletionClient from utils import FileLogHandler @@ -121,6 +122,27 @@ async def test_magentic_one_group_chat_basic() -> None: assert result.messages[4].content == "print('Hello, world!')" assert result.stop_reason is not None and result.stop_reason == "Because" + # Test save and load. + state = await team.save_state() + team2 = MagenticOneGroupChat(participants=[agent_1, agent_2, agent_3, agent_4], model_client=model_client) + await team2.load_state(state) + state2 = await team2.save_state() + assert state == state2 + manager_1 = await team._runtime.try_get_underlying_agent_instance( # pyright: ignore + AgentId("group_chat_manager", team._team_id), # pyright: ignore + MagenticOneOrchestrator, # pyright: ignore + ) # pyright: ignore + manager_2 = await team2._runtime.try_get_underlying_agent_instance( # pyright: ignore + AgentId("group_chat_manager", team2._team_id), # pyright: ignore + MagenticOneOrchestrator, # pyright: ignore + ) # pyright: ignore + assert manager_1._message_thread == manager_2._message_thread # pyright: ignore + assert manager_1._task == manager_2._task # pyright: ignore + assert manager_1._facts == manager_2._facts # pyright: ignore + assert manager_1._plan == manager_2._plan # pyright: ignore + assert manager_1._n_rounds == manager_2._n_rounds # pyright: ignore + assert manager_1._n_stalls == manager_2._n_stalls # pyright: ignore + @pytest.mark.asyncio async def test_magentic_one_group_chat_with_stalls() -> None: diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/index.md b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/index.md index 597063a0c01a..4ffadc2a9add 100644 --- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/index.md +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/index.md @@ -48,6 +48,12 @@ A dynamic team that uses handoffs to pass tasks between agents. How to build custom agents. ::: +:::{grid-item-card} {fas}`users;pst-color-primary` State Management +:link: ./state.html + +How to manage state in agents and teams. +::: + :::: ```{toctree} @@ -61,4 +67,5 @@ selector-group-chat swarm termination custom-agents +state ``` diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/state.ipynb b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/state.ipynb new file mode 100644 index 000000000000..e09c255f5a42 --- /dev/null +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/state.ipynb @@ -0,0 +1,299 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Managing State \n", + "\n", + "So far, we have discussed how to build components in a multi-agent application - agents, teams, termination conditions. In many cases, it is useful to save the state of these components to disk and load them back later. This is particularly useful in a web application where stateless endpoints respond to requests and need to load the state of the application from persistent storage.\n", + "\n", + "In this notebook, we will discuss how to save and load the state of agents, teams, and termination conditions. \n", + " \n", + "\n", + "## Saving and Loading Agents\n", + "\n", + "We can get the state of an agent by calling {py:meth}`~autogen_agentchat.agents.AssistantAgent.save_state` method on \n", + "an {py:class}`~autogen_agentchat.agents.AssistantAgent`. " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "In Tanganyika's depths so wide and deep, \n", + "Ancient secrets in still waters sleep, \n", + "Ripples tell tales that time longs to keep. \n" + ] + } + ], + "source": [ + "from autogen_agentchat.agents import AssistantAgent\n", + "from autogen_agentchat.conditions import MaxMessageTermination\n", + "from autogen_agentchat.messages import TextMessage\n", + "from autogen_agentchat.teams import RoundRobinGroupChat\n", + "from autogen_agentchat.ui import Console\n", + "from autogen_core import CancellationToken\n", + "from autogen_ext.models import OpenAIChatCompletionClient\n", + "\n", + "assistant_agent = AssistantAgent(\n", + " name=\"assistant_agent\",\n", + " system_message=\"You are a helpful assistant\",\n", + " model_client=OpenAIChatCompletionClient(\n", + " model=\"gpt-4o-2024-08-06\",\n", + " # api_key=\"YOUR_API_KEY\",\n", + " ),\n", + ")\n", + "\n", + "# Use asyncio.run(...) when running in a script.\n", + "response = await assistant_agent.on_messages(\n", + " [TextMessage(content=\"Write a 3 line poem on lake tangayika\", source=\"user\")], CancellationToken()\n", + ")\n", + "print(response.chat_message.content)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'type': 'AssistantAgentState', 'version': '1.0.0', 'llm_messages': [{'content': 'Write a 3 line poem on lake tangayika', 'source': 'user', 'type': 'UserMessage'}, {'content': \"In Tanganyika's depths so wide and deep, \\nAncient secrets in still waters sleep, \\nRipples tell tales that time longs to keep. \", 'source': 'assistant_agent', 'type': 'AssistantMessage'}]}\n" + ] + } + ], + "source": [ + "agent_state = await assistant_agent.save_state()\n", + "print(agent_state)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The last line of the poem I wrote was: \n", + "\"Ripples tell tales that time longs to keep.\"\n" + ] + } + ], + "source": [ + "new_assistant_agent = AssistantAgent(\n", + " name=\"assistant_agent\",\n", + " system_message=\"You are a helpful assistant\",\n", + " model_client=OpenAIChatCompletionClient(\n", + " model=\"gpt-4o-2024-08-06\",\n", + " ),\n", + ")\n", + "await new_assistant_agent.load_state(agent_state)\n", + "\n", + "# Use asyncio.run(...) when running in a script.\n", + "response = await new_assistant_agent.on_messages(\n", + " [TextMessage(content=\"What was the last line of the previous poem you wrote\", source=\"user\")], CancellationToken()\n", + ")\n", + "print(response.chat_message.content)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```{note}\n", + "For {py:class}`~autogen_agentchat.agents.AssistantAgent`, its state consists of the model_context.\n", + "If your write your own custom agent, consider overriding the {py:meth}`~autogen_agentchat.agents.BaseChatAgent.save_state` and {py:meth}`~autogen_agentchat.agents.BaseChatAgent.load_state` methods to customize the behavior. The default implementations save and load an empty state.\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Saving and Loading Teams \n", + "\n", + "We can get the state of a team by calling `save_state` method on the team and load it back by calling `load_state` method on the team. \n", + "\n", + "When we call `save_state` on a team, it saves the state of all the agents in the team.\n", + "\n", + "We will begin by creating a simple {py:class}`~autogen_agentchat.teams.RoundRobinGroupChat` team with a single agent and ask it to write a poem. " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- user ----------\n", + "Write a beautiful poem 3-line about lake tangayika\n", + "---------- assistant_agent ----------\n", + "In Tanganyika's depths, where light gently weaves, \n", + "Silver reflections dance on ancient water's face, \n", + "Whispered stories of time in the rippling leaves. \n", + "[Prompt tokens: 29, Completion tokens: 36]\n", + "---------- Summary ----------\n", + "Number of messages: 2\n", + "Finish reason: Maximum number of messages 2 reached, current message count: 2\n", + "Total prompt tokens: 29\n", + "Total completion tokens: 36\n", + "Duration: 1.16 seconds\n" + ] + } + ], + "source": [ + "# Define a team.\n", + "assistant_agent = AssistantAgent(\n", + " name=\"assistant_agent\",\n", + " system_message=\"You are a helpful assistant\",\n", + " model_client=OpenAIChatCompletionClient(\n", + " model=\"gpt-4o-2024-08-06\",\n", + " ),\n", + ")\n", + "agent_team = RoundRobinGroupChat([assistant_agent], termination_condition=MaxMessageTermination(max_messages=2))\n", + "\n", + "# Run the team and stream messages to the console.\n", + "stream = agent_team.run_stream(task=\"Write a beautiful poem 3-line about lake tangayika\")\n", + "\n", + "# Use asyncio.run(...) when running in a script.\n", + "await Console(stream)\n", + "\n", + "# Save the state of the agent team.\n", + "team_state = await agent_team.save_state()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we reset the team (simulating instantiation of the team), and ask the question `What was the last line of the poem you wrote?`, we see that the team is unable to accomplish this as there is no reference to the previous run." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- user ----------\n", + "What was the last line of the poem you wrote?\n", + "---------- assistant_agent ----------\n", + "I don't write poems on my own, but I can help create one with you or try to recall a specific poem if you have one in mind. Let me know what you'd like to do!\n", + "[Prompt tokens: 28, Completion tokens: 39]\n", + "---------- Summary ----------\n", + "Number of messages: 2\n", + "Finish reason: Maximum number of messages 2 reached, current message count: 2\n", + "Total prompt tokens: 28\n", + "Total completion tokens: 39\n", + "Duration: 0.95 seconds\n" + ] + }, + { + "data": { + "text/plain": [ + "TaskResult(messages=[TextMessage(source='user', models_usage=None, type='TextMessage', content='What was the last line of the poem you wrote?'), TextMessage(source='assistant_agent', models_usage=RequestUsage(prompt_tokens=28, completion_tokens=39), type='TextMessage', content=\"I don't write poems on my own, but I can help create one with you or try to recall a specific poem if you have one in mind. Let me know what you'd like to do!\")], stop_reason='Maximum number of messages 2 reached, current message count: 2')" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "await agent_team.reset()\n", + "stream = agent_team.run_stream(task=\"What was the last line of the poem you wrote?\")\n", + "await Console(stream)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we load the state of the team and ask the same question. We see that the team is able to accurately return the last line of the poem it wrote.\n", + "\n", + "Note: You can serialize the state of the team to a file and load it back later." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'type': 'TeamState', 'version': '1.0.0', 'agent_states': {'group_chat_manager/c80054be-efb2-4bc7-ba0d-900962092c44': {'type': 'RoundRobinManagerState', 'version': '1.0.0', 'message_thread': [{'source': 'user', 'models_usage': None, 'type': 'TextMessage', 'content': 'Write a beautiful poem 3-line about lake tangayika'}, {'source': 'assistant_agent', 'models_usage': {'prompt_tokens': 29, 'completion_tokens': 36}, 'type': 'TextMessage', 'content': \"In Tanganyika's depths, where light gently weaves, \\nSilver reflections dance on ancient water's face, \\nWhispered stories of time in the rippling leaves. \"}], 'current_turn': 0, 'next_speaker_index': 0}, 'collect_output_messages/c80054be-efb2-4bc7-ba0d-900962092c44': {}, 'assistant_agent/c80054be-efb2-4bc7-ba0d-900962092c44': {'type': 'ChatAgentContainerState', 'version': '1.0.0', 'agent_state': {'type': 'AssistantAgentState', 'version': '1.0.0', 'llm_messages': [{'content': 'Write a beautiful poem 3-line about lake tangayika', 'source': 'user', 'type': 'UserMessage'}, {'content': \"In Tanganyika's depths, where light gently weaves, \\nSilver reflections dance on ancient water's face, \\nWhispered stories of time in the rippling leaves. \", 'source': 'assistant_agent', 'type': 'AssistantMessage'}]}, 'message_buffer': []}}, 'team_id': 'c80054be-efb2-4bc7-ba0d-900962092c44'}\n", + "---------- user ----------\n", + "What was the last line of the poem you wrote?\n", + "---------- assistant_agent ----------\n", + "The last line of the poem I wrote was: \n", + "\"Whispered stories of time in the rippling leaves.\"\n", + "[Prompt tokens: 88, Completion tokens: 24]\n", + "---------- Summary ----------\n", + "Number of messages: 2\n", + "Finish reason: Maximum number of messages 2 reached, current message count: 2\n", + "Total prompt tokens: 88\n", + "Total completion tokens: 24\n", + "Duration: 0.79 seconds\n" + ] + }, + { + "data": { + "text/plain": [ + "TaskResult(messages=[TextMessage(source='user', models_usage=None, type='TextMessage', content='What was the last line of the poem you wrote?'), TextMessage(source='assistant_agent', models_usage=RequestUsage(prompt_tokens=88, completion_tokens=24), type='TextMessage', content='The last line of the poem I wrote was: \\n\"Whispered stories of time in the rippling leaves.\"')], stop_reason='Maximum number of messages 2 reached, current message count: 2')" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "print(team_state)\n", + "\n", + "# Load team state.\n", + "await agent_team.load_state(team_state)\n", + "stream = agent_team.run_stream(task=\"What was the last line of the poem you wrote?\")\n", + "await Console(stream)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/cookbook/tool-use-with-intervention.ipynb b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/cookbook/tool-use-with-intervention.ipynb index f5ac5bb2a9f4..98fd748d34ff 100644 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/cookbook/tool-use-with-intervention.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/cookbook/tool-use-with-intervention.ipynb @@ -1,283 +1,283 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# User Approval for Tool Execution using Intervention Handler\n", - "\n", - "This cookbook shows how to intercept the tool execution using\n", - "an intervention hanlder, and prompt the user for permission to execute the tool." - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": {}, - "outputs": [], - "source": [ - "from dataclasses import dataclass\n", - "from typing import Any, List\n", - "\n", - "from autogen_core import AgentId, AgentType, FunctionCall, MessageContext, RoutedAgent, message_handler\n", - "from autogen_core.application import SingleThreadedAgentRuntime\n", - "from autogen_core.base.intervention import DefaultInterventionHandler, DropMessage\n", - "from autogen_core.components.models import (\n", - " ChatCompletionClient,\n", - " LLMMessage,\n", - " SystemMessage,\n", - " UserMessage,\n", - ")\n", - "from autogen_core.components.tools import PythonCodeExecutionTool, ToolSchema\n", - "from autogen_core.tool_agent import ToolAgent, ToolException, tool_agent_caller_loop\n", - "from autogen_ext.code_executors import DockerCommandLineCodeExecutor\n", - "from autogen_ext.models import OpenAIChatCompletionClient" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's define a simple message type that carries a string content." - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": {}, - "outputs": [], - "source": [ - "@dataclass\n", - "class Message:\n", - " content: str" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's create a simple tool use agent that is capable of using tools through a\n", - "{py:class}`~autogen_core.components.tool_agent.ToolAgent`." - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "metadata": {}, - "outputs": [], - "source": [ - "class ToolUseAgent(RoutedAgent):\n", - " \"\"\"An agent that uses tools to perform tasks. It executes the tools\n", - " by itself by sending the tool execution task to a ToolAgent.\"\"\"\n", - "\n", - " def __init__(\n", - " self,\n", - " description: str,\n", - " system_messages: List[SystemMessage],\n", - " model_client: ChatCompletionClient,\n", - " tool_schema: List[ToolSchema],\n", - " tool_agent_type: AgentType,\n", - " ) -> None:\n", - " super().__init__(description)\n", - " self._model_client = model_client\n", - " self._system_messages = system_messages\n", - " self._tool_schema = tool_schema\n", - " self._tool_agent_id = AgentId(type=tool_agent_type, key=self.id.key)\n", - "\n", - " @message_handler\n", - " async def handle_user_message(self, message: Message, ctx: MessageContext) -> Message:\n", - " \"\"\"Handle a user message, execute the model and tools, and returns the response.\"\"\"\n", - " session: List[LLMMessage] = [UserMessage(content=message.content, source=\"User\")]\n", - " # Use the tool agent to execute the tools, and get the output messages.\n", - " output_messages = await tool_agent_caller_loop(\n", - " self,\n", - " tool_agent_id=self._tool_agent_id,\n", - " model_client=self._model_client,\n", - " input_messages=session,\n", - " tool_schema=self._tool_schema,\n", - " cancellation_token=ctx.cancellation_token,\n", - " )\n", - " # Extract the final response from the output messages.\n", - " final_response = output_messages[-1].content\n", - " assert isinstance(final_response, str)\n", - " return Message(content=final_response)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The tool use agent sends tool call requests to the tool agent to execute tools,\n", - "so we can intercept the messages sent by the tool use agent to the tool agent\n", - "to prompt the user for permission to execute the tool.\n", - "\n", - "Let's create an intervention handler that intercepts the messages and prompts\n", - "user for before allowing the tool execution." - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "metadata": {}, - "outputs": [], - "source": [ - "class ToolInterventionHandler(DefaultInterventionHandler):\n", - " async def on_send(self, message: Any, *, sender: AgentId | None, recipient: AgentId) -> Any | type[DropMessage]:\n", - " if isinstance(message, FunctionCall):\n", - " # Request user prompt for tool execution.\n", - " user_input = input(\n", - " f\"Function call: {message.name}\\nArguments: {message.arguments}\\nDo you want to execute the tool? (y/n): \"\n", - " )\n", - " if user_input.strip().lower() != \"y\":\n", - " raise ToolException(content=\"User denied tool execution.\", call_id=message.id)\n", - " return message" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, we can create a runtime with the intervention handler registered." - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "metadata": {}, - "outputs": [], - "source": [ - "# Create the runtime with the intervention handler.\n", - "runtime = SingleThreadedAgentRuntime(intervention_handlers=[ToolInterventionHandler()])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In this example, we will use a tool for Python code execution.\n", - "First, we create a Docker-based command-line code executor\n", - "using {py:class}`~autogen_core.components.code_executor.docker_executorCommandLineCodeExecutor`,\n", - "and then use it to instantiate a built-in Python code execution tool\n", - "{py:class}`~autogen_core.components.tools.PythonCodeExecutionTool`\n", - "that runs code in a Docker container." - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "metadata": {}, - "outputs": [], - "source": [ - "# Create the docker executor for the Python code execution tool.\n", - "docker_executor = DockerCommandLineCodeExecutor()\n", - "\n", - "# Create the Python code execution tool.\n", - "python_tool = PythonCodeExecutionTool(executor=docker_executor)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Register the agents with tools and tool schema." - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "AgentType(type='tool_enabled_agent')" - ] - }, - "execution_count": 33, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Register agents.\n", - "tool_agent_type = await ToolAgent.register(\n", - " runtime,\n", - " \"tool_executor_agent\",\n", - " lambda: ToolAgent(\n", - " description=\"Tool Executor Agent\",\n", - " tools=[python_tool],\n", - " ),\n", - ")\n", - "await ToolUseAgent.register(\n", - " runtime,\n", - " \"tool_enabled_agent\",\n", - " lambda: ToolUseAgent(\n", - " description=\"Tool Use Agent\",\n", - " system_messages=[SystemMessage(\"You are a helpful AI Assistant. Use your tools to solve problems.\")],\n", - " model_client=OpenAIChatCompletionClient(model=\"gpt-4o-mini\"),\n", - " tool_schema=[python_tool.schema],\n", - " tool_agent_type=tool_agent_type,\n", - " ),\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Run the agents by starting the runtime and sending a message to the tool use agent.\n", - "The intervention handler will prompt you for permission to execute the tool." - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The output of the code is: **Hello, World!**\n" - ] - } - ], - "source": [ - "# Start the runtime and the docker executor.\n", - "await docker_executor.start()\n", - "runtime.start()\n", - "\n", - "# Send a task to the tool user.\n", - "response = await runtime.send_message(\n", - " Message(\"Run the following Python code: print('Hello, World!')\"), AgentId(\"tool_enabled_agent\", \"default\")\n", - ")\n", - "print(response.content)\n", - "\n", - "# Stop the runtime and the docker executor.\n", - "await runtime.stop()\n", - "await docker_executor.stop()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } - }, - "nbformat": 4, - "nbformat_minor": 2 + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# User Approval for Tool Execution using Intervention Handler\n", + "\n", + "This cookbook shows how to intercept the tool execution using\n", + "an intervention hanlder, and prompt the user for permission to execute the tool." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [], + "source": [ + "from dataclasses import dataclass\n", + "from typing import Any, List\n", + "\n", + "from autogen_core import AgentId, AgentType, FunctionCall, MessageContext, RoutedAgent, message_handler\n", + "from autogen_core.application import SingleThreadedAgentRuntime\n", + "from autogen_core.base.intervention import DefaultInterventionHandler, DropMessage\n", + "from autogen_core.components.models import (\n", + " ChatCompletionClient,\n", + " LLMMessage,\n", + " SystemMessage,\n", + " UserMessage,\n", + ")\n", + "from autogen_core.components.tools import PythonCodeExecutionTool, ToolSchema\n", + "from autogen_core.tool_agent import ToolAgent, ToolException, tool_agent_caller_loop\n", + "from autogen_ext.code_executors import DockerCommandLineCodeExecutor\n", + "from autogen_ext.models import OpenAIChatCompletionClient" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's define a simple message type that carries a string content." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [], + "source": [ + "@dataclass\n", + "class Message:\n", + " content: str" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's create a simple tool use agent that is capable of using tools through a\n", + "{py:class}`~autogen_core.components.tool_agent.ToolAgent`." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [], + "source": [ + "class ToolUseAgent(RoutedAgent):\n", + " \"\"\"An agent that uses tools to perform tasks. It executes the tools\n", + " by itself by sending the tool execution task to a ToolAgent.\"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " description: str,\n", + " system_messages: List[SystemMessage],\n", + " model_client: ChatCompletionClient,\n", + " tool_schema: List[ToolSchema],\n", + " tool_agent_type: AgentType,\n", + " ) -> None:\n", + " super().__init__(description)\n", + " self._model_client = model_client\n", + " self._system_messages = system_messages\n", + " self._tool_schema = tool_schema\n", + " self._tool_agent_id = AgentId(type=tool_agent_type, key=self.id.key)\n", + "\n", + " @message_handler\n", + " async def handle_user_message(self, message: Message, ctx: MessageContext) -> Message:\n", + " \"\"\"Handle a user message, execute the model and tools, and returns the response.\"\"\"\n", + " session: List[LLMMessage] = [UserMessage(content=message.content, source=\"User\")]\n", + " # Use the tool agent to execute the tools, and get the output messages.\n", + " output_messages = await tool_agent_caller_loop(\n", + " self,\n", + " tool_agent_id=self._tool_agent_id,\n", + " model_client=self._model_client,\n", + " input_messages=session,\n", + " tool_schema=self._tool_schema,\n", + " cancellation_token=ctx.cancellation_token,\n", + " )\n", + " # Extract the final response from the output messages.\n", + " final_response = output_messages[-1].content\n", + " assert isinstance(final_response, str)\n", + " return Message(content=final_response)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The tool use agent sends tool call requests to the tool agent to execute tools,\n", + "so we can intercept the messages sent by the tool use agent to the tool agent\n", + "to prompt the user for permission to execute the tool.\n", + "\n", + "Let's create an intervention handler that intercepts the messages and prompts\n", + "user for before allowing the tool execution." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "class ToolInterventionHandler(DefaultInterventionHandler):\n", + " async def on_send(self, message: Any, *, sender: AgentId | None, recipient: AgentId) -> Any | type[DropMessage]:\n", + " if isinstance(message, FunctionCall):\n", + " # Request user prompt for tool execution.\n", + " user_input = input(\n", + " f\"Function call: {message.name}\\nArguments: {message.arguments}\\nDo you want to execute the tool? (y/n): \"\n", + " )\n", + " if user_input.strip().lower() != \"y\":\n", + " raise ToolException(content=\"User denied tool execution.\", call_id=message.id)\n", + " return message" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we can create a runtime with the intervention handler registered." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [], + "source": [ + "# Create the runtime with the intervention handler.\n", + "runtime = SingleThreadedAgentRuntime(intervention_handlers=[ToolInterventionHandler()])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this example, we will use a tool for Python code execution.\n", + "First, we create a Docker-based command-line code executor\n", + "using {py:class}`~autogen_core.components.code_executor.docker_executorCommandLineCodeExecutor`,\n", + "and then use it to instantiate a built-in Python code execution tool\n", + "{py:class}`~autogen_core.components.tools.PythonCodeExecutionTool`\n", + "that runs code in a Docker container." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [], + "source": [ + "# Create the docker executor for the Python code execution tool.\n", + "docker_executor = DockerCommandLineCodeExecutor()\n", + "\n", + "# Create the Python code execution tool.\n", + "python_tool = PythonCodeExecutionTool(executor=docker_executor)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Register the agents with tools and tool schema." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AgentType(type='tool_enabled_agent')" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Register agents.\n", + "tool_agent_type = await ToolAgent.register(\n", + " runtime,\n", + " \"tool_executor_agent\",\n", + " lambda: ToolAgent(\n", + " description=\"Tool Executor Agent\",\n", + " tools=[python_tool],\n", + " ),\n", + ")\n", + "await ToolUseAgent.register(\n", + " runtime,\n", + " \"tool_enabled_agent\",\n", + " lambda: ToolUseAgent(\n", + " description=\"Tool Use Agent\",\n", + " system_messages=[SystemMessage(content=\"You are a helpful AI Assistant. Use your tools to solve problems.\")],\n", + " model_client=OpenAIChatCompletionClient(model=\"gpt-4o-mini\"),\n", + " tool_schema=[python_tool.schema],\n", + " tool_agent_type=tool_agent_type,\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Run the agents by starting the runtime and sending a message to the tool use agent.\n", + "The intervention handler will prompt you for permission to execute the tool." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The output of the code is: **Hello, World!**\n" + ] + } + ], + "source": [ + "# Start the runtime and the docker executor.\n", + "await docker_executor.start()\n", + "runtime.start()\n", + "\n", + "# Send a task to the tool user.\n", + "response = await runtime.send_message(\n", + " Message(\"Run the following Python code: print('Hello, World!')\"), AgentId(\"tool_enabled_agent\", \"default\")\n", + ")\n", + "print(response.content)\n", + "\n", + "# Stop the runtime and the docker executor.\n", + "await runtime.stop()\n", + "await docker_executor.stop()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/cookbook/topic-subscription-scenarios.ipynb b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/cookbook/topic-subscription-scenarios.ipynb index 70e2b98474ee..fafb88b6d093 100644 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/cookbook/topic-subscription-scenarios.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/cookbook/topic-subscription-scenarios.ipynb @@ -1,601 +1,601 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Topic and Subscription Example Scenarios\n", - "\n", - "### Introduction\n", - "\n", - "In this cookbook, we explore how broadcasting works for agent communication in AutoGen using four different broadcasting scenarios. These scenarios illustrate various ways to handle and distribute messages among agents. We'll use a consistent example of a tax management company processing client requests to demonstrate each scenario.\n", - "\n", - "### Scenario Overview\n", - "\n", - "Imagine a tax management company that offers various services to clients, such as tax planning, dispute resolution, compliance, and preparation. The company employs a team of tax specialists, each with expertise in one of these areas, and a tax system manager who oversees the operations.\n", - "\n", - "Clients submit requests that need to be processed by the appropriate specialists. The communication between the clients, the tax system manager, and the tax specialists is handled through broadcasting in this system.\n", - "\n", - "We'll explore how different broadcasting scenarios affect the way messages are distributed among agents and how they can be used to tailor the communication flow to specific needs.\n", - "\n", - "---\n", - "\n", - "### Broadcasting Scenarios Overview\n", - "\n", - "We will cover the following broadcasting scenarios:\n", - "\n", - "1. **Single-Tenant, Single Scope of Publishing**\n", - "2. **Multi-Tenant, Single Scope of Publishing**\n", - "3. **Single-Tenant, Multiple Scopes of Publishing**\n", - "4. **Multi-Tenant, Multiple Scopes of Publishing**\n", - "\n", - "\n", - "Each scenario represents a different approach to message distribution and agent interaction within the system. By understanding these scenarios, you can design agent communication strategies that best fit your application's requirements." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import asyncio\n", - "from dataclasses import dataclass\n", - "from enum import Enum\n", - "from typing import List\n", - "\n", - "from autogen_core import MessageContext, RoutedAgent, TopicId, TypeSubscription, message_handler\n", - "from autogen_core._default_subscription import DefaultSubscription\n", - "from autogen_core._default_topic import DefaultTopicId\n", - "from autogen_core.application import SingleThreadedAgentRuntime\n", - "from autogen_core.components.models import (\n", - " SystemMessage,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "class TaxSpecialty(str, Enum):\n", - " PLANNING = \"planning\"\n", - " DISPUTE_RESOLUTION = \"dispute_resolution\"\n", - " COMPLIANCE = \"compliance\"\n", - " PREPARATION = \"preparation\"\n", - "\n", - "\n", - "@dataclass\n", - "class ClientRequest:\n", - " content: str\n", - "\n", - "\n", - "@dataclass\n", - "class RequestAssessment:\n", - " content: str\n", - "\n", - "\n", - "class TaxSpecialist(RoutedAgent):\n", - " def __init__(\n", - " self,\n", - " description: str,\n", - " specialty: TaxSpecialty,\n", - " system_messages: List[SystemMessage],\n", - " ) -> None:\n", - " super().__init__(description)\n", - " self.specialty = specialty\n", - " self._system_messages = system_messages\n", - " self._memory: List[ClientRequest] = []\n", - "\n", - " @message_handler\n", - " async def handle_message(self, message: ClientRequest, ctx: MessageContext) -> None:\n", - " # Process the client request.\n", - " print(f\"\\n{'='*50}\\nTax specialist {self.id} with specialty {self.specialty}:\\n{message.content}\")\n", - " # Send a response back to the manager\n", - " if ctx.topic_id is None:\n", - " raise ValueError(\"Topic ID is required for broadcasting\")\n", - " await self.publish_message(\n", - " message=RequestAssessment(content=f\"I can handle this request in {self.specialty}.\"),\n", - " topic_id=ctx.topic_id,\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 1. Single-Tenant, Single Scope of Publishing\n", - "\n", - "#### Scenarios Explanation\n", - "In the single-tenant, single scope of publishing scenario:\n", - "\n", - "- All agents operate within a single tenant (e.g., one client or user session).\n", - "- Messages are published to a single topic, and all agents subscribe to this topic.\n", - "- Every agent receives every message that gets published to the topic.\n", - "\n", - "This scenario is suitable for situations where all agents need to be aware of all messages, and there's no need to isolate communication between different groups of agents or sessions.\n", - "\n", - "#### Application in the Tax Specialist Company\n", - "\n", - "In our tax specialist company, this scenario implies:\n", - "\n", - "- All tax specialists receive every client request and internal message.\n", - "- All agents collaborate closely, with full visibility of all communications.\n", - "- Useful for tasks or teams where all agents need to be aware of all messages.\n", - "\n", - "#### How the Scenario Works\n", - "\n", - "- Subscriptions: All agents use the default subscription(e.g., \"default\").\n", - "- Publishing: Messages are published to the default topic.\n", - "- Message Handling: Each agent decides whether to act on a message based on its content and available handlers.\n", - "\n", - "#### Benefits\n", - "- Simplicity: Easy to set up and understand.\n", - "- Collaboration: Promotes transparency and collaboration among agents.\n", - "- Flexibility: Agents can dynamically decide which messages to process.\n", - "\n", - "#### Considerations\n", - "- Scalability: May not scale well with a large number of agents or messages.\n", - "- Efficiency: Agents may receive many irrelevant messages, leading to unnecessary processing." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "==================================================\n", - "Tax specialist TaxSpecialist_1:default with specialty TaxSpecialty.PLANNING:\n", - "I need to have my tax for 2024 prepared.\n", - "\n", - "==================================================\n", - "Tax specialist TaxSpecialist_2:default with specialty TaxSpecialty.DISPUTE_RESOLUTION:\n", - "I need to have my tax for 2024 prepared.\n" - ] - } - ], - "source": [ - "async def run_single_tenant_single_scope() -> None:\n", - " # Create the runtime.\n", - " runtime = SingleThreadedAgentRuntime()\n", - "\n", - " # Register TaxSpecialist agents for each specialty\n", - " specialist_agent_type_1 = \"TaxSpecialist_1\"\n", - " specialist_agent_type_2 = \"TaxSpecialist_2\"\n", - " await TaxSpecialist.register(\n", - " runtime=runtime,\n", - " type=specialist_agent_type_1,\n", - " factory=lambda: TaxSpecialist(\n", - " description=\"A tax specialist 1\",\n", - " specialty=TaxSpecialty.PLANNING,\n", - " system_messages=[SystemMessage(\"You are a tax specialist.\")],\n", - " ),\n", - " )\n", - "\n", - " await TaxSpecialist.register(\n", - " runtime=runtime,\n", - " type=specialist_agent_type_2,\n", - " factory=lambda: TaxSpecialist(\n", - " description=\"A tax specialist 2\",\n", - " specialty=TaxSpecialty.DISPUTE_RESOLUTION,\n", - " system_messages=[SystemMessage(\"You are a tax specialist.\")],\n", - " ),\n", - " )\n", - "\n", - " # Add default subscriptions for each agent type\n", - " await runtime.add_subscription(DefaultSubscription(agent_type=specialist_agent_type_1))\n", - " await runtime.add_subscription(DefaultSubscription(agent_type=specialist_agent_type_2))\n", - "\n", - " # Start the runtime and send a message to agents on default topic\n", - " runtime.start()\n", - " await runtime.publish_message(ClientRequest(\"I need to have my tax for 2024 prepared.\"), topic_id=DefaultTopicId())\n", - " await runtime.stop_when_idle()\n", - "\n", - "\n", - "await run_single_tenant_single_scope()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 2. Multi-Tenant, Single Scope of Publishing\n", - "\n", - "#### Scenario Explanation\n", - "\n", - "In the multi-tenant, single scope of publishing scenario:\n", - "\n", - "- There are multiple tenants (e.g., multiple clients or user sessions).\n", - "- Each tenant has its own isolated topic through the topic source.\n", - "- All agents within a tenant subscribe to the tenant's topic. If needed, new agent instances are created for each tenant.\n", - "- Messages are only visible to agents within the same tenant.\n", - "\n", - "This scenario is useful when you need to isolate communication between different tenants but want all agents within a tenant to be aware of all messages.\n", - "\n", - "#### Application in the Tax Specialist Company\n", - "\n", - "In this scenario:\n", - "\n", - "- The company serves multiple clients (tenants) simultaneously.\n", - "- For each client, a dedicated set of agent instances is created.\n", - "- Each client's communication is isolated from others.\n", - "- All agents for a client receive messages published to that client's topic.\n", - "\n", - "#### How the Scenario Works\n", - "\n", - "- Subscriptions: Agents subscribe to topics based on the tenant's identity.\n", - "- Publishing: Messages are published to the tenant-specific topic.\n", - "- Message Handling: Agents only receive messages relevant to their tenant.\n", - "\n", - "#### Benefits\n", - "- Tenant Isolation: Ensures data privacy and separation between clients.\n", - "- Collaboration Within Tenant: Agents can collaborate freely within their tenant.\n", - "\n", - "#### Considerations\n", - "- Complexity: Requires managing multiple sets of agents and topics.\n", - "- Resource Usage: More agent instances may consume additional resources." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "==================================================\n", - "Tax specialist TaxSpecialist_planning:ClientABC with specialty TaxSpecialty.PLANNING:\n", - "ClientABC requires tax services.\n", - "\n", - "==================================================\n", - "Tax specialist TaxSpecialist_dispute_resolution:ClientABC with specialty TaxSpecialty.DISPUTE_RESOLUTION:\n", - "ClientABC requires tax services.\n", - "\n", - "==================================================\n", - "Tax specialist TaxSpecialist_compliance:ClientABC with specialty TaxSpecialty.COMPLIANCE:\n", - "ClientABC requires tax services.\n", - "\n", - "==================================================\n", - "Tax specialist TaxSpecialist_preparation:ClientABC with specialty TaxSpecialty.PREPARATION:\n", - "ClientABC requires tax services.\n", - "\n", - "==================================================\n", - "Tax specialist TaxSpecialist_planning:ClientXYZ with specialty TaxSpecialty.PLANNING:\n", - "ClientXYZ requires tax services.\n", - "\n", - "==================================================\n", - "Tax specialist TaxSpecialist_dispute_resolution:ClientXYZ with specialty TaxSpecialty.DISPUTE_RESOLUTION:\n", - "ClientXYZ requires tax services.\n", - "\n", - "==================================================\n", - "Tax specialist TaxSpecialist_compliance:ClientXYZ with specialty TaxSpecialty.COMPLIANCE:\n", - "ClientXYZ requires tax services.\n", - "\n", - "==================================================\n", - "Tax specialist TaxSpecialist_preparation:ClientXYZ with specialty TaxSpecialty.PREPARATION:\n", - "ClientXYZ requires tax services.\n" - ] - } - ], - "source": [ - "async def run_multi_tenant_single_scope() -> None:\n", - " # Create the runtime\n", - " runtime = SingleThreadedAgentRuntime()\n", - "\n", - " # List of clients (tenants)\n", - " tenants = [\"ClientABC\", \"ClientXYZ\"]\n", - "\n", - " # Initialize sessions and map the topic type to each TaxSpecialist agent type\n", - " for specialty in TaxSpecialty:\n", - " specialist_agent_type = f\"TaxSpecialist_{specialty.value}\"\n", - " await TaxSpecialist.register(\n", - " runtime=runtime,\n", - " type=specialist_agent_type,\n", - " factory=lambda specialty=specialty: TaxSpecialist( # type: ignore\n", - " description=f\"A tax specialist in {specialty.value}.\",\n", - " specialty=specialty,\n", - " system_messages=[SystemMessage(f\"You are a tax specialist in {specialty.value}.\")],\n", - " ),\n", - " )\n", - " specialist_subscription = DefaultSubscription(agent_type=specialist_agent_type)\n", - " await runtime.add_subscription(specialist_subscription)\n", - "\n", - " # Start the runtime\n", - " runtime.start()\n", - "\n", - " # Publish client requests to their respective topics\n", - " for tenant in tenants:\n", - " topic_source = tenant # The topic source is the client name\n", - " topic_id = DefaultTopicId(source=topic_source)\n", - " await runtime.publish_message(\n", - " ClientRequest(f\"{tenant} requires tax services.\"),\n", - " topic_id=topic_id,\n", - " )\n", - "\n", - " # Allow time for message processing\n", - " await asyncio.sleep(1)\n", - "\n", - " # Stop the runtime when idle\n", - " await runtime.stop_when_idle()\n", - "\n", - "\n", - "await run_multi_tenant_single_scope()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 3. Single-Tenant, Multiple Scopes of Publishing\n", - "\n", - "#### Scenario Explanation\n", - "\n", - "In the single-tenant, multiple scopes of publishing scenario:\n", - "\n", - "- All agents operate within a single tenant.\n", - "- Messages are published to different topics.\n", - "- Agents subscribe to specific topics relevant to their role or specialty.\n", - "- Messages are directed to subsets of agents based on the topic.\n", - "\n", - "This scenario allows for targeted communication within a tenant, enabling more granular control over message distribution.\n", - "\n", - "#### Application in the Tax Management Company\n", - "\n", - "In this scenario:\n", - "\n", - "- The tax system manager communicates with specific specialists based on their specialties.\n", - "- Different topics represent different specialties (e.g., \"planning\", \"compliance\").\n", - "- Specialists subscribe only to the topic that matches their specialty.\n", - "- The manager publishes messages to specific topics to reach the intended specialists.\n", - "\n", - "#### How the Scenario Works\n", - "\n", - "- Subscriptions: Agents subscribe to topics corresponding to their specialties.\n", - "- Publishing: Messages are published to topics based on the intended recipients.\n", - "- Message Handling: Only agents subscribed to a topic receive its messages.\n", - "#### Benefits\n", - "\n", - "- Targeted Communication: Messages reach only the relevant agents.\n", - "- Efficiency: Reduces unnecessary message processing by agents.\n", - "\n", - "#### Considerations\n", - "\n", - "- Setup Complexity: Requires careful management of topics and subscriptions.\n", - "- Flexibility: Changes in communication scenarios may require updating subscriptions." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "==================================================\n", - "Tax specialist TaxSpecialist_planning:default with specialty TaxSpecialty.PLANNING:\n", - "I need assistance with planning taxes.\n", - "\n", - "==================================================\n", - "Tax specialist TaxSpecialist_dispute_resolution:default with specialty TaxSpecialty.DISPUTE_RESOLUTION:\n", - "I need assistance with dispute_resolution taxes.\n", - "\n", - "==================================================\n", - "Tax specialist TaxSpecialist_compliance:default with specialty TaxSpecialty.COMPLIANCE:\n", - "I need assistance with compliance taxes.\n", - "\n", - "==================================================\n", - "Tax specialist TaxSpecialist_preparation:default with specialty TaxSpecialty.PREPARATION:\n", - "I need assistance with preparation taxes.\n" - ] - } - ], - "source": [ - "async def run_single_tenant_multiple_scope() -> None:\n", - " # Create the runtime\n", - " runtime = SingleThreadedAgentRuntime()\n", - " # Register TaxSpecialist agents for each specialty and add subscriptions\n", - " for specialty in TaxSpecialty:\n", - " specialist_agent_type = f\"TaxSpecialist_{specialty.value}\"\n", - " await TaxSpecialist.register(\n", - " runtime=runtime,\n", - " type=specialist_agent_type,\n", - " factory=lambda specialty=specialty: TaxSpecialist( # type: ignore\n", - " description=f\"A tax specialist in {specialty.value}.\",\n", - " specialty=specialty,\n", - " system_messages=[SystemMessage(f\"You are a tax specialist in {specialty.value}.\")],\n", - " ),\n", - " )\n", - " specialist_subscription = TypeSubscription(topic_type=specialty.value, agent_type=specialist_agent_type)\n", - " await runtime.add_subscription(specialist_subscription)\n", - "\n", - " # Start the runtime\n", - " runtime.start()\n", - "\n", - " # Publish a ClientRequest to each specialist's topic\n", - " for specialty in TaxSpecialty:\n", - " topic_id = TopicId(type=specialty.value, source=\"default\")\n", - " await runtime.publish_message(\n", - " ClientRequest(f\"I need assistance with {specialty.value} taxes.\"),\n", - " topic_id=topic_id,\n", - " )\n", - "\n", - " # Allow time for message processing\n", - " await asyncio.sleep(1)\n", - "\n", - " # Stop the runtime when idle\n", - " await runtime.stop_when_idle()\n", - "\n", - "\n", - "await run_single_tenant_multiple_scope()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 4. Multi-Tenant, Multiple Scopes of Publishing\n", - "\n", - "#### Scenario Explanation\n", - "\n", - "In the multi-tenant, multiple scopes of publishing scenario:\n", - "\n", - "- There are multiple tenants, each with their own set of agents.\n", - "- Messages are published to multiple topics within each tenant.\n", - "- Agents subscribe to tenant-specific topics relevant to their role.\n", - "- Combines tenant isolation with targeted communication.\n", - "\n", - "This scenario provides the highest level of control over message distribution, suitable for complex systems with multiple clients and specialized communication needs.\n", - "\n", - "#### Application in the Tax Management Company\n", - "\n", - "In this scenario:\n", - "\n", - "- The company serves multiple clients, each with dedicated agent instances.\n", - "- Within each client, agents communicate using multiple topics based on specialties.\n", - "- For example, Client A's planning specialist subscribes to the \"planning\" topic with source \"ClientA\".\n", - "- The tax system manager for each client communicates with their specialists using tenant-specific topics.\n", - "\n", - "#### How the Scenario Works\n", - "\n", - "- Subscriptions: Agents subscribe to topics based on both tenant identity and specialty.\n", - "- Publishing: Messages are published to tenant-specific and specialty-specific topics.\n", - "- Message Handling: Only agents matching the tenant and topic receive messages.\n", - "\n", - "#### Benefits\n", - "\n", - "- Complete Isolation: Ensures both tenant and communication isolation.\n", - "- Granular Control: Enables precise routing of messages to intended agents.\n", - "\n", - "#### Considerations\n", - "\n", - "- Complexity: Requires careful management of topics, tenants, and subscriptions.\n", - "- Resource Usage: Increased number of agent instances and topics may impact resources." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "==================================================\n", - "Tax specialist TaxSpecialist_planning:ClientABC with specialty TaxSpecialty.PLANNING:\n", - "ClientABC needs assistance with planning taxes.\n", - "\n", - "==================================================\n", - "Tax specialist TaxSpecialist_dispute_resolution:ClientABC with specialty TaxSpecialty.DISPUTE_RESOLUTION:\n", - "ClientABC needs assistance with dispute_resolution taxes.\n", - "\n", - "==================================================\n", - "Tax specialist TaxSpecialist_compliance:ClientABC with specialty TaxSpecialty.COMPLIANCE:\n", - "ClientABC needs assistance with compliance taxes.\n", - "\n", - "==================================================\n", - "Tax specialist TaxSpecialist_preparation:ClientABC with specialty TaxSpecialty.PREPARATION:\n", - "ClientABC needs assistance with preparation taxes.\n", - "\n", - "==================================================\n", - "Tax specialist TaxSpecialist_planning:ClientXYZ with specialty TaxSpecialty.PLANNING:\n", - "ClientXYZ needs assistance with planning taxes.\n", - "\n", - "==================================================\n", - "Tax specialist TaxSpecialist_dispute_resolution:ClientXYZ with specialty TaxSpecialty.DISPUTE_RESOLUTION:\n", - "ClientXYZ needs assistance with dispute_resolution taxes.\n", - "\n", - "==================================================\n", - "Tax specialist TaxSpecialist_compliance:ClientXYZ with specialty TaxSpecialty.COMPLIANCE:\n", - "ClientXYZ needs assistance with compliance taxes.\n", - "\n", - "==================================================\n", - "Tax specialist TaxSpecialist_preparation:ClientXYZ with specialty TaxSpecialty.PREPARATION:\n", - "ClientXYZ needs assistance with preparation taxes.\n" - ] - } - ], - "source": [ - "async def run_multi_tenant_multiple_scope() -> None:\n", - " # Create the runtime\n", - " runtime = SingleThreadedAgentRuntime()\n", - "\n", - " # Define TypeSubscriptions for each specialty and tenant\n", - " tenants = [\"ClientABC\", \"ClientXYZ\"]\n", - "\n", - " # Initialize agents for all specialties and add type subscriptions\n", - " for specialty in TaxSpecialty:\n", - " specialist_agent_type = f\"TaxSpecialist_{specialty.value}\"\n", - " await TaxSpecialist.register(\n", - " runtime=runtime,\n", - " type=specialist_agent_type,\n", - " factory=lambda specialty=specialty: TaxSpecialist( # type: ignore\n", - " description=f\"A tax specialist in {specialty.value}.\",\n", - " specialty=specialty,\n", - " system_messages=[SystemMessage(f\"You are a tax specialist in {specialty.value}.\")],\n", - " ),\n", - " )\n", - " for tenant in tenants:\n", - " specialist_subscription = TypeSubscription(\n", - " topic_type=f\"{tenant}_{specialty.value}\", agent_type=specialist_agent_type\n", - " )\n", - " await runtime.add_subscription(specialist_subscription)\n", - "\n", - " # Start the runtime\n", - " runtime.start()\n", - "\n", - " # Send messages for each tenant to each specialty\n", - " for tenant in tenants:\n", - " for specialty in TaxSpecialty:\n", - " topic_id = TopicId(type=f\"{tenant}_{specialty.value}\", source=tenant)\n", - " await runtime.publish_message(\n", - " ClientRequest(f\"{tenant} needs assistance with {specialty.value} taxes.\"),\n", - " topic_id=topic_id,\n", - " )\n", - "\n", - " # Allow time for message processing\n", - " await asyncio.sleep(1)\n", - "\n", - " # Stop the runtime when idle\n", - " await runtime.stop_when_idle()\n", - "\n", - "\n", - "await run_multi_tenant_multiple_scope()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.6" - } - }, - "nbformat": 4, - "nbformat_minor": 2 + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Topic and Subscription Example Scenarios\n", + "\n", + "### Introduction\n", + "\n", + "In this cookbook, we explore how broadcasting works for agent communication in AutoGen using four different broadcasting scenarios. These scenarios illustrate various ways to handle and distribute messages among agents. We'll use a consistent example of a tax management company processing client requests to demonstrate each scenario.\n", + "\n", + "### Scenario Overview\n", + "\n", + "Imagine a tax management company that offers various services to clients, such as tax planning, dispute resolution, compliance, and preparation. The company employs a team of tax specialists, each with expertise in one of these areas, and a tax system manager who oversees the operations.\n", + "\n", + "Clients submit requests that need to be processed by the appropriate specialists. The communication between the clients, the tax system manager, and the tax specialists is handled through broadcasting in this system.\n", + "\n", + "We'll explore how different broadcasting scenarios affect the way messages are distributed among agents and how they can be used to tailor the communication flow to specific needs.\n", + "\n", + "---\n", + "\n", + "### Broadcasting Scenarios Overview\n", + "\n", + "We will cover the following broadcasting scenarios:\n", + "\n", + "1. **Single-Tenant, Single Scope of Publishing**\n", + "2. **Multi-Tenant, Single Scope of Publishing**\n", + "3. **Single-Tenant, Multiple Scopes of Publishing**\n", + "4. **Multi-Tenant, Multiple Scopes of Publishing**\n", + "\n", + "\n", + "Each scenario represents a different approach to message distribution and agent interaction within the system. By understanding these scenarios, you can design agent communication strategies that best fit your application's requirements." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import asyncio\n", + "from dataclasses import dataclass\n", + "from enum import Enum\n", + "from typing import List\n", + "\n", + "from autogen_core import MessageContext, RoutedAgent, TopicId, TypeSubscription, message_handler\n", + "from autogen_core._default_subscription import DefaultSubscription\n", + "from autogen_core._default_topic import DefaultTopicId\n", + "from autogen_core.application import SingleThreadedAgentRuntime\n", + "from autogen_core.components.models import (\n", + " SystemMessage,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "class TaxSpecialty(str, Enum):\n", + " PLANNING = \"planning\"\n", + " DISPUTE_RESOLUTION = \"dispute_resolution\"\n", + " COMPLIANCE = \"compliance\"\n", + " PREPARATION = \"preparation\"\n", + "\n", + "\n", + "@dataclass\n", + "class ClientRequest:\n", + " content: str\n", + "\n", + "\n", + "@dataclass\n", + "class RequestAssessment:\n", + " content: str\n", + "\n", + "\n", + "class TaxSpecialist(RoutedAgent):\n", + " def __init__(\n", + " self,\n", + " description: str,\n", + " specialty: TaxSpecialty,\n", + " system_messages: List[SystemMessage],\n", + " ) -> None:\n", + " super().__init__(description)\n", + " self.specialty = specialty\n", + " self._system_messages = system_messages\n", + " self._memory: List[ClientRequest] = []\n", + "\n", + " @message_handler\n", + " async def handle_message(self, message: ClientRequest, ctx: MessageContext) -> None:\n", + " # Process the client request.\n", + " print(f\"\\n{'='*50}\\nTax specialist {self.id} with specialty {self.specialty}:\\n{message.content}\")\n", + " # Send a response back to the manager\n", + " if ctx.topic_id is None:\n", + " raise ValueError(\"Topic ID is required for broadcasting\")\n", + " await self.publish_message(\n", + " message=RequestAssessment(content=f\"I can handle this request in {self.specialty}.\"),\n", + " topic_id=ctx.topic_id,\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1. Single-Tenant, Single Scope of Publishing\n", + "\n", + "#### Scenarios Explanation\n", + "In the single-tenant, single scope of publishing scenario:\n", + "\n", + "- All agents operate within a single tenant (e.g., one client or user session).\n", + "- Messages are published to a single topic, and all agents subscribe to this topic.\n", + "- Every agent receives every message that gets published to the topic.\n", + "\n", + "This scenario is suitable for situations where all agents need to be aware of all messages, and there's no need to isolate communication between different groups of agents or sessions.\n", + "\n", + "#### Application in the Tax Specialist Company\n", + "\n", + "In our tax specialist company, this scenario implies:\n", + "\n", + "- All tax specialists receive every client request and internal message.\n", + "- All agents collaborate closely, with full visibility of all communications.\n", + "- Useful for tasks or teams where all agents need to be aware of all messages.\n", + "\n", + "#### How the Scenario Works\n", + "\n", + "- Subscriptions: All agents use the default subscription(e.g., \"default\").\n", + "- Publishing: Messages are published to the default topic.\n", + "- Message Handling: Each agent decides whether to act on a message based on its content and available handlers.\n", + "\n", + "#### Benefits\n", + "- Simplicity: Easy to set up and understand.\n", + "- Collaboration: Promotes transparency and collaboration among agents.\n", + "- Flexibility: Agents can dynamically decide which messages to process.\n", + "\n", + "#### Considerations\n", + "- Scalability: May not scale well with a large number of agents or messages.\n", + "- Efficiency: Agents may receive many irrelevant messages, leading to unnecessary processing." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "==================================================\n", + "Tax specialist TaxSpecialist_1:default with specialty TaxSpecialty.PLANNING:\n", + "I need to have my tax for 2024 prepared.\n", + "\n", + "==================================================\n", + "Tax specialist TaxSpecialist_2:default with specialty TaxSpecialty.DISPUTE_RESOLUTION:\n", + "I need to have my tax for 2024 prepared.\n" + ] + } + ], + "source": [ + "async def run_single_tenant_single_scope() -> None:\n", + " # Create the runtime.\n", + " runtime = SingleThreadedAgentRuntime()\n", + "\n", + " # Register TaxSpecialist agents for each specialty\n", + " specialist_agent_type_1 = \"TaxSpecialist_1\"\n", + " specialist_agent_type_2 = \"TaxSpecialist_2\"\n", + " await TaxSpecialist.register(\n", + " runtime=runtime,\n", + " type=specialist_agent_type_1,\n", + " factory=lambda: TaxSpecialist(\n", + " description=\"A tax specialist 1\",\n", + " specialty=TaxSpecialty.PLANNING,\n", + " system_messages=[SystemMessage(content=\"You are a tax specialist.\")],\n", + " ),\n", + " )\n", + "\n", + " await TaxSpecialist.register(\n", + " runtime=runtime,\n", + " type=specialist_agent_type_2,\n", + " factory=lambda: TaxSpecialist(\n", + " description=\"A tax specialist 2\",\n", + " specialty=TaxSpecialty.DISPUTE_RESOLUTION,\n", + " system_messages=[SystemMessage(content=\"You are a tax specialist.\")],\n", + " ),\n", + " )\n", + "\n", + " # Add default subscriptions for each agent type\n", + " await runtime.add_subscription(DefaultSubscription(agent_type=specialist_agent_type_1))\n", + " await runtime.add_subscription(DefaultSubscription(agent_type=specialist_agent_type_2))\n", + "\n", + " # Start the runtime and send a message to agents on default topic\n", + " runtime.start()\n", + " await runtime.publish_message(ClientRequest(\"I need to have my tax for 2024 prepared.\"), topic_id=DefaultTopicId())\n", + " await runtime.stop_when_idle()\n", + "\n", + "\n", + "await run_single_tenant_single_scope()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2. Multi-Tenant, Single Scope of Publishing\n", + "\n", + "#### Scenario Explanation\n", + "\n", + "In the multi-tenant, single scope of publishing scenario:\n", + "\n", + "- There are multiple tenants (e.g., multiple clients or user sessions).\n", + "- Each tenant has its own isolated topic through the topic source.\n", + "- All agents within a tenant subscribe to the tenant's topic. If needed, new agent instances are created for each tenant.\n", + "- Messages are only visible to agents within the same tenant.\n", + "\n", + "This scenario is useful when you need to isolate communication between different tenants but want all agents within a tenant to be aware of all messages.\n", + "\n", + "#### Application in the Tax Specialist Company\n", + "\n", + "In this scenario:\n", + "\n", + "- The company serves multiple clients (tenants) simultaneously.\n", + "- For each client, a dedicated set of agent instances is created.\n", + "- Each client's communication is isolated from others.\n", + "- All agents for a client receive messages published to that client's topic.\n", + "\n", + "#### How the Scenario Works\n", + "\n", + "- Subscriptions: Agents subscribe to topics based on the tenant's identity.\n", + "- Publishing: Messages are published to the tenant-specific topic.\n", + "- Message Handling: Agents only receive messages relevant to their tenant.\n", + "\n", + "#### Benefits\n", + "- Tenant Isolation: Ensures data privacy and separation between clients.\n", + "- Collaboration Within Tenant: Agents can collaborate freely within their tenant.\n", + "\n", + "#### Considerations\n", + "- Complexity: Requires managing multiple sets of agents and topics.\n", + "- Resource Usage: More agent instances may consume additional resources." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "==================================================\n", + "Tax specialist TaxSpecialist_planning:ClientABC with specialty TaxSpecialty.PLANNING:\n", + "ClientABC requires tax services.\n", + "\n", + "==================================================\n", + "Tax specialist TaxSpecialist_dispute_resolution:ClientABC with specialty TaxSpecialty.DISPUTE_RESOLUTION:\n", + "ClientABC requires tax services.\n", + "\n", + "==================================================\n", + "Tax specialist TaxSpecialist_compliance:ClientABC with specialty TaxSpecialty.COMPLIANCE:\n", + "ClientABC requires tax services.\n", + "\n", + "==================================================\n", + "Tax specialist TaxSpecialist_preparation:ClientABC with specialty TaxSpecialty.PREPARATION:\n", + "ClientABC requires tax services.\n", + "\n", + "==================================================\n", + "Tax specialist TaxSpecialist_planning:ClientXYZ with specialty TaxSpecialty.PLANNING:\n", + "ClientXYZ requires tax services.\n", + "\n", + "==================================================\n", + "Tax specialist TaxSpecialist_dispute_resolution:ClientXYZ with specialty TaxSpecialty.DISPUTE_RESOLUTION:\n", + "ClientXYZ requires tax services.\n", + "\n", + "==================================================\n", + "Tax specialist TaxSpecialist_compliance:ClientXYZ with specialty TaxSpecialty.COMPLIANCE:\n", + "ClientXYZ requires tax services.\n", + "\n", + "==================================================\n", + "Tax specialist TaxSpecialist_preparation:ClientXYZ with specialty TaxSpecialty.PREPARATION:\n", + "ClientXYZ requires tax services.\n" + ] + } + ], + "source": [ + "async def run_multi_tenant_single_scope() -> None:\n", + " # Create the runtime\n", + " runtime = SingleThreadedAgentRuntime()\n", + "\n", + " # List of clients (tenants)\n", + " tenants = [\"ClientABC\", \"ClientXYZ\"]\n", + "\n", + " # Initialize sessions and map the topic type to each TaxSpecialist agent type\n", + " for specialty in TaxSpecialty:\n", + " specialist_agent_type = f\"TaxSpecialist_{specialty.value}\"\n", + " await TaxSpecialist.register(\n", + " runtime=runtime,\n", + " type=specialist_agent_type,\n", + " factory=lambda specialty=specialty: TaxSpecialist( # type: ignore\n", + " description=f\"A tax specialist in {specialty.value}.\",\n", + " specialty=specialty,\n", + " system_messages=[SystemMessage(content=f\"You are a tax specialist in {specialty.value}.\")],\n", + " ),\n", + " )\n", + " specialist_subscription = DefaultSubscription(agent_type=specialist_agent_type)\n", + " await runtime.add_subscription(specialist_subscription)\n", + "\n", + " # Start the runtime\n", + " runtime.start()\n", + "\n", + " # Publish client requests to their respective topics\n", + " for tenant in tenants:\n", + " topic_source = tenant # The topic source is the client name\n", + " topic_id = DefaultTopicId(source=topic_source)\n", + " await runtime.publish_message(\n", + " ClientRequest(f\"{tenant} requires tax services.\"),\n", + " topic_id=topic_id,\n", + " )\n", + "\n", + " # Allow time for message processing\n", + " await asyncio.sleep(1)\n", + "\n", + " # Stop the runtime when idle\n", + " await runtime.stop_when_idle()\n", + "\n", + "\n", + "await run_multi_tenant_single_scope()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3. Single-Tenant, Multiple Scopes of Publishing\n", + "\n", + "#### Scenario Explanation\n", + "\n", + "In the single-tenant, multiple scopes of publishing scenario:\n", + "\n", + "- All agents operate within a single tenant.\n", + "- Messages are published to different topics.\n", + "- Agents subscribe to specific topics relevant to their role or specialty.\n", + "- Messages are directed to subsets of agents based on the topic.\n", + "\n", + "This scenario allows for targeted communication within a tenant, enabling more granular control over message distribution.\n", + "\n", + "#### Application in the Tax Management Company\n", + "\n", + "In this scenario:\n", + "\n", + "- The tax system manager communicates with specific specialists based on their specialties.\n", + "- Different topics represent different specialties (e.g., \"planning\", \"compliance\").\n", + "- Specialists subscribe only to the topic that matches their specialty.\n", + "- The manager publishes messages to specific topics to reach the intended specialists.\n", + "\n", + "#### How the Scenario Works\n", + "\n", + "- Subscriptions: Agents subscribe to topics corresponding to their specialties.\n", + "- Publishing: Messages are published to topics based on the intended recipients.\n", + "- Message Handling: Only agents subscribed to a topic receive its messages.\n", + "#### Benefits\n", + "\n", + "- Targeted Communication: Messages reach only the relevant agents.\n", + "- Efficiency: Reduces unnecessary message processing by agents.\n", + "\n", + "#### Considerations\n", + "\n", + "- Setup Complexity: Requires careful management of topics and subscriptions.\n", + "- Flexibility: Changes in communication scenarios may require updating subscriptions." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "==================================================\n", + "Tax specialist TaxSpecialist_planning:default with specialty TaxSpecialty.PLANNING:\n", + "I need assistance with planning taxes.\n", + "\n", + "==================================================\n", + "Tax specialist TaxSpecialist_dispute_resolution:default with specialty TaxSpecialty.DISPUTE_RESOLUTION:\n", + "I need assistance with dispute_resolution taxes.\n", + "\n", + "==================================================\n", + "Tax specialist TaxSpecialist_compliance:default with specialty TaxSpecialty.COMPLIANCE:\n", + "I need assistance with compliance taxes.\n", + "\n", + "==================================================\n", + "Tax specialist TaxSpecialist_preparation:default with specialty TaxSpecialty.PREPARATION:\n", + "I need assistance with preparation taxes.\n" + ] + } + ], + "source": [ + "async def run_single_tenant_multiple_scope() -> None:\n", + " # Create the runtime\n", + " runtime = SingleThreadedAgentRuntime()\n", + " # Register TaxSpecialist agents for each specialty and add subscriptions\n", + " for specialty in TaxSpecialty:\n", + " specialist_agent_type = f\"TaxSpecialist_{specialty.value}\"\n", + " await TaxSpecialist.register(\n", + " runtime=runtime,\n", + " type=specialist_agent_type,\n", + " factory=lambda specialty=specialty: TaxSpecialist( # type: ignore\n", + " description=f\"A tax specialist in {specialty.value}.\",\n", + " specialty=specialty,\n", + " system_messages=[SystemMessage(content=f\"You are a tax specialist in {specialty.value}.\")],\n", + " ),\n", + " )\n", + " specialist_subscription = TypeSubscription(topic_type=specialty.value, agent_type=specialist_agent_type)\n", + " await runtime.add_subscription(specialist_subscription)\n", + "\n", + " # Start the runtime\n", + " runtime.start()\n", + "\n", + " # Publish a ClientRequest to each specialist's topic\n", + " for specialty in TaxSpecialty:\n", + " topic_id = TopicId(type=specialty.value, source=\"default\")\n", + " await runtime.publish_message(\n", + " ClientRequest(f\"I need assistance with {specialty.value} taxes.\"),\n", + " topic_id=topic_id,\n", + " )\n", + "\n", + " # Allow time for message processing\n", + " await asyncio.sleep(1)\n", + "\n", + " # Stop the runtime when idle\n", + " await runtime.stop_when_idle()\n", + "\n", + "\n", + "await run_single_tenant_multiple_scope()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4. Multi-Tenant, Multiple Scopes of Publishing\n", + "\n", + "#### Scenario Explanation\n", + "\n", + "In the multi-tenant, multiple scopes of publishing scenario:\n", + "\n", + "- There are multiple tenants, each with their own set of agents.\n", + "- Messages are published to multiple topics within each tenant.\n", + "- Agents subscribe to tenant-specific topics relevant to their role.\n", + "- Combines tenant isolation with targeted communication.\n", + "\n", + "This scenario provides the highest level of control over message distribution, suitable for complex systems with multiple clients and specialized communication needs.\n", + "\n", + "#### Application in the Tax Management Company\n", + "\n", + "In this scenario:\n", + "\n", + "- The company serves multiple clients, each with dedicated agent instances.\n", + "- Within each client, agents communicate using multiple topics based on specialties.\n", + "- For example, Client A's planning specialist subscribes to the \"planning\" topic with source \"ClientA\".\n", + "- The tax system manager for each client communicates with their specialists using tenant-specific topics.\n", + "\n", + "#### How the Scenario Works\n", + "\n", + "- Subscriptions: Agents subscribe to topics based on both tenant identity and specialty.\n", + "- Publishing: Messages are published to tenant-specific and specialty-specific topics.\n", + "- Message Handling: Only agents matching the tenant and topic receive messages.\n", + "\n", + "#### Benefits\n", + "\n", + "- Complete Isolation: Ensures both tenant and communication isolation.\n", + "- Granular Control: Enables precise routing of messages to intended agents.\n", + "\n", + "#### Considerations\n", + "\n", + "- Complexity: Requires careful management of topics, tenants, and subscriptions.\n", + "- Resource Usage: Increased number of agent instances and topics may impact resources." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "==================================================\n", + "Tax specialist TaxSpecialist_planning:ClientABC with specialty TaxSpecialty.PLANNING:\n", + "ClientABC needs assistance with planning taxes.\n", + "\n", + "==================================================\n", + "Tax specialist TaxSpecialist_dispute_resolution:ClientABC with specialty TaxSpecialty.DISPUTE_RESOLUTION:\n", + "ClientABC needs assistance with dispute_resolution taxes.\n", + "\n", + "==================================================\n", + "Tax specialist TaxSpecialist_compliance:ClientABC with specialty TaxSpecialty.COMPLIANCE:\n", + "ClientABC needs assistance with compliance taxes.\n", + "\n", + "==================================================\n", + "Tax specialist TaxSpecialist_preparation:ClientABC with specialty TaxSpecialty.PREPARATION:\n", + "ClientABC needs assistance with preparation taxes.\n", + "\n", + "==================================================\n", + "Tax specialist TaxSpecialist_planning:ClientXYZ with specialty TaxSpecialty.PLANNING:\n", + "ClientXYZ needs assistance with planning taxes.\n", + "\n", + "==================================================\n", + "Tax specialist TaxSpecialist_dispute_resolution:ClientXYZ with specialty TaxSpecialty.DISPUTE_RESOLUTION:\n", + "ClientXYZ needs assistance with dispute_resolution taxes.\n", + "\n", + "==================================================\n", + "Tax specialist TaxSpecialist_compliance:ClientXYZ with specialty TaxSpecialty.COMPLIANCE:\n", + "ClientXYZ needs assistance with compliance taxes.\n", + "\n", + "==================================================\n", + "Tax specialist TaxSpecialist_preparation:ClientXYZ with specialty TaxSpecialty.PREPARATION:\n", + "ClientXYZ needs assistance with preparation taxes.\n" + ] + } + ], + "source": [ + "async def run_multi_tenant_multiple_scope() -> None:\n", + " # Create the runtime\n", + " runtime = SingleThreadedAgentRuntime()\n", + "\n", + " # Define TypeSubscriptions for each specialty and tenant\n", + " tenants = [\"ClientABC\", \"ClientXYZ\"]\n", + "\n", + " # Initialize agents for all specialties and add type subscriptions\n", + " for specialty in TaxSpecialty:\n", + " specialist_agent_type = f\"TaxSpecialist_{specialty.value}\"\n", + " await TaxSpecialist.register(\n", + " runtime=runtime,\n", + " type=specialist_agent_type,\n", + " factory=lambda specialty=specialty: TaxSpecialist( # type: ignore\n", + " description=f\"A tax specialist in {specialty.value}.\",\n", + " specialty=specialty,\n", + " system_messages=[SystemMessage(content=f\"You are a tax specialist in {specialty.value}.\")],\n", + " ),\n", + " )\n", + " for tenant in tenants:\n", + " specialist_subscription = TypeSubscription(\n", + " topic_type=f\"{tenant}_{specialty.value}\", agent_type=specialist_agent_type\n", + " )\n", + " await runtime.add_subscription(specialist_subscription)\n", + "\n", + " # Start the runtime\n", + " runtime.start()\n", + "\n", + " # Send messages for each tenant to each specialty\n", + " for tenant in tenants:\n", + " for specialty in TaxSpecialty:\n", + " topic_id = TopicId(type=f\"{tenant}_{specialty.value}\", source=tenant)\n", + " await runtime.publish_message(\n", + " ClientRequest(f\"{tenant} needs assistance with {specialty.value} taxes.\"),\n", + " topic_id=topic_id,\n", + " )\n", + "\n", + " # Allow time for message processing\n", + " await asyncio.sleep(1)\n", + "\n", + " # Stop the runtime when idle\n", + " await runtime.stop_when_idle()\n", + "\n", + "\n", + "await run_multi_tenant_multiple_scope()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/group-chat.ipynb b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/group-chat.ipynb index e5b61381213d..1f1d06f7038b 100644 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/group-chat.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/group-chat.ipynb @@ -158,7 +158,7 @@ " super().__init__(description=description)\n", " self._group_chat_topic_type = group_chat_topic_type\n", " self._model_client = model_client\n", - " self._system_message = SystemMessage(system_message)\n", + " self._system_message = SystemMessage(content=system_message)\n", " self._chat_history: List[LLMMessage] = []\n", "\n", " @message_handler\n", @@ -427,7 +427,7 @@ "Read the above conversation. Then select the next role from {participants} to play. Only return the role.\n", "\"\"\"\n", " system_message = SystemMessage(\n", - " selector_prompt.format(\n", + " content=selector_prompt.format(\n", " roles=roles,\n", " history=history,\n", " participants=str(\n", diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/mixture-of-agents.ipynb b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/mixture-of-agents.ipynb index c008eff71834..735f5a61f79a 100644 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/mixture-of-agents.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/mixture-of-agents.ipynb @@ -1,520 +1,520 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Mixture of Agents\n", - "\n", - "[Mixture of Agents](https://arxiv.org/abs/2406.04692) is a multi-agent design pattern\n", - "that models after the feed-forward neural network architecture.\n", - "\n", - "The pattern consists of two types of agents: worker agents and a single orchestrator agent.\n", - "Worker agents are organized into multiple layers, with each layer consisting of a fixed number of worker agents.\n", - "Messages from the worker agents in a previous layer are concatenated and sent to\n", - "all the worker agents in the next layer.\n", - "\n", - "This example implements the Mixture of Agents pattern using the core library\n", - "following the [original implementation](https://github.com/togethercomputer/moa) of multi-layer mixture of agents.\n", - "\n", - "Here is a high-level procedure overview of the pattern:\n", - "1. The orchestrator agent takes input a user task and first dispatches it to the worker agents in the first layer.\n", - "2. The worker agents in the first layer process the task and return the results to the orchestrator agent.\n", - "3. The orchestrator agent then synthesizes the results from the first layer and dispatches an updated task with the previous results to the worker agents in the second layer.\n", - "4. The process continues until the final layer is reached.\n", - "5. In the final layer, the orchestrator agent aggregates the results from previous layer and returns a single final result to the user.\n", - "\n", - "We use the direct messaging API {py:meth}`~autogen_core.base.BaseAgent.send_message` to implement this pattern.\n", - "This makes it easier to add more features like worker task cancellation and error handling in the future." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import asyncio\n", - "from dataclasses import dataclass\n", - "from typing import List\n", - "\n", - "from autogen_core import AgentId, MessageContext, RoutedAgent, message_handler\n", - "from autogen_core.application import SingleThreadedAgentRuntime\n", - "from autogen_core.components.models import ChatCompletionClient, SystemMessage, UserMessage\n", - "from autogen_ext.models import OpenAIChatCompletionClient" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Message Protocol\n", - "\n", - "The agents communicate using the following messages:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "@dataclass\n", - "class WorkerTask:\n", - " task: str\n", - " previous_results: List[str]\n", - "\n", - "\n", - "@dataclass\n", - "class WorkerTaskResult:\n", - " result: str\n", - "\n", - "\n", - "@dataclass\n", - "class UserTask:\n", - " task: str\n", - "\n", - "\n", - "@dataclass\n", - "class FinalResult:\n", - " result: str" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Worker Agent\n", - "\n", - "Each worker agent receives a task from the orchestrator agent and processes them\n", - "indepedently.\n", - "Once the task is completed, the worker agent returns the result." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "class WorkerAgent(RoutedAgent):\n", - " def __init__(\n", - " self,\n", - " model_client: ChatCompletionClient,\n", - " ) -> None:\n", - " super().__init__(description=\"Worker Agent\")\n", - " self._model_client = model_client\n", - "\n", - " @message_handler\n", - " async def handle_task(self, message: WorkerTask, ctx: MessageContext) -> WorkerTaskResult:\n", - " if message.previous_results:\n", - " # If previous results are provided, we need to synthesize them to create a single prompt.\n", - " system_prompt = \"You have been provided with a set of responses from various open-source models to the latest user query. Your task is to synthesize these responses into a single, high-quality response. It is crucial to critically evaluate the information provided in these responses, recognizing that some of it may be biased or incorrect. Your response should not simply replicate the given answers but should offer a refined, accurate, and comprehensive reply to the instruction. Ensure your response is well-structured, coherent, and adheres to the highest standards of accuracy and reliability.\\n\\nResponses from models:\"\n", - " system_prompt += \"\\n\" + \"\\n\\n\".join([f\"{i+1}. {r}\" for i, r in enumerate(message.previous_results)])\n", - " model_result = await self._model_client.create(\n", - " [SystemMessage(system_prompt), UserMessage(content=message.task, source=\"user\")]\n", - " )\n", - " else:\n", - " # If no previous results are provided, we can simply pass the user query to the model.\n", - " model_result = await self._model_client.create([UserMessage(content=message.task, source=\"user\")])\n", - " assert isinstance(model_result.content, str)\n", - " print(f\"{'-'*80}\\nWorker-{self.id}:\\n{model_result.content}\")\n", - " return WorkerTaskResult(result=model_result.content)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Orchestrator Agent\n", - "\n", - "The orchestrator agent receives tasks from the user and distributes them to the worker agents,\n", - "iterating over multiple layers of worker agents. Once all worker agents have processed the task,\n", - "the orchestrator agent aggregates the results and publishes the final result." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "class OrchestratorAgent(RoutedAgent):\n", - " def __init__(\n", - " self,\n", - " model_client: ChatCompletionClient,\n", - " worker_agent_types: List[str],\n", - " num_layers: int,\n", - " ) -> None:\n", - " super().__init__(description=\"Aggregator Agent\")\n", - " self._model_client = model_client\n", - " self._worker_agent_types = worker_agent_types\n", - " self._num_layers = num_layers\n", - "\n", - " @message_handler\n", - " async def handle_task(self, message: UserTask, ctx: MessageContext) -> FinalResult:\n", - " print(f\"{'-'*80}\\nOrchestrator-{self.id}:\\nReceived task: {message.task}\")\n", - " # Create task for the first layer.\n", - " worker_task = WorkerTask(task=message.task, previous_results=[])\n", - " # Iterate over layers.\n", - " for i in range(self._num_layers - 1):\n", - " # Assign workers for this layer.\n", - " worker_ids = [\n", - " AgentId(worker_type, f\"{self.id.key}/layer_{i}/worker_{j}\")\n", - " for j, worker_type in enumerate(self._worker_agent_types)\n", - " ]\n", - " # Dispatch tasks to workers.\n", - " print(f\"{'-'*80}\\nOrchestrator-{self.id}:\\nDispatch to workers at layer {i}\")\n", - " results = await asyncio.gather(*[self.send_message(worker_task, worker_id) for worker_id in worker_ids])\n", - " print(f\"{'-'*80}\\nOrchestrator-{self.id}:\\nReceived results from workers at layer {i}\")\n", - " # Prepare task for the next layer.\n", - " worker_task = WorkerTask(task=message.task, previous_results=[r.result for r in results])\n", - " # Perform final aggregation.\n", - " print(f\"{'-'*80}\\nOrchestrator-{self.id}:\\nPerforming final aggregation\")\n", - " system_prompt = \"You have been provided with a set of responses from various open-source models to the latest user query. Your task is to synthesize these responses into a single, high-quality response. It is crucial to critically evaluate the information provided in these responses, recognizing that some of it may be biased or incorrect. Your response should not simply replicate the given answers but should offer a refined, accurate, and comprehensive reply to the instruction. Ensure your response is well-structured, coherent, and adheres to the highest standards of accuracy and reliability.\\n\\nResponses from models:\"\n", - " system_prompt += \"\\n\" + \"\\n\\n\".join([f\"{i+1}. {r}\" for i, r in enumerate(worker_task.previous_results)])\n", - " model_result = await self._model_client.create(\n", - " [SystemMessage(system_prompt), UserMessage(content=message.task, source=\"user\")]\n", - " )\n", - " assert isinstance(model_result.content, str)\n", - " return FinalResult(result=model_result.content)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Running Mixture of Agents\n", - "\n", - "Let's run the mixture of agents on a math task. You can change the task to make it more challenging, for example, by trying tasks from the [International Mathematical Olympiad](https://www.imo-official.org/problems.aspx)." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "task = (\n", - " \"I have 432 cookies, and divide them 3:4:2 between Alice, Bob, and Charlie. How many cookies does each person get?\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's set up the runtime with 3 layers of worker agents, each layer consisting of 3 worker agents.\n", - "We only need to register a single worker agent types, \"worker\", because we are using\n", - "the same model client configuration (i.e., gpt-4o-mini) for all worker agents.\n", - "If you want to use different models, you will need to register multiple worker agent types,\n", - "one for each model, and update the `worker_agent_types` list in the orchestrator agent's\n", - "factory function.\n", - "\n", - "The instances of worker agents are automatically created when the orchestrator agent\n", - "dispatches tasks to them.\n", - "See [Agent Identity and Lifecycle](../core-concepts/agent-identity-and-lifecycle.md)\n", - "for more information on agent lifecycle." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "--------------------------------------------------------------------------------\n", - "Orchestrator-orchestrator:default:\n", - "Received task: I have 432 cookies, and divide them 3:4:2 between Alice, Bob, and Charlie. How many cookies does each person get?\n", - "--------------------------------------------------------------------------------\n", - "Orchestrator-orchestrator:default:\n", - "Dispatch to workers at layer 0\n", - "--------------------------------------------------------------------------------\n", - "Worker-worker:default/layer_0/worker_1:\n", - "To divide 432 cookies in the ratio of 3:4:2 between Alice, Bob, and Charlie, you first need to determine the total number of parts in the ratio.\n", - "\n", - "Add the parts together:\n", - "\\[ 3 + 4 + 2 = 9 \\]\n", - "\n", - "Now, you can find the value of one part by dividing the total number of cookies by the total number of parts:\n", - "\\[ \\text{Value of one part} = \\frac{432}{9} = 48 \\]\n", - "\n", - "Now, multiply the value of one part by the number of parts for each person:\n", - "\n", - "- For Alice (3 parts):\n", - "\\[ 3 \\times 48 = 144 \\]\n", - "\n", - "- For Bob (4 parts):\n", - "\\[ 4 \\times 48 = 192 \\]\n", - "\n", - "- For Charlie (2 parts):\n", - "\\[ 2 \\times 48 = 96 \\]\n", - "\n", - "Thus, the number of cookies each person gets is:\n", - "- Alice: 144 cookies\n", - "- Bob: 192 cookies\n", - "- Charlie: 96 cookies\n", - "--------------------------------------------------------------------------------\n", - "Worker-worker:default/layer_0/worker_0:\n", - "To divide 432 cookies in the ratio of 3:4:2 between Alice, Bob, and Charlie, we will first determine the total number of parts in the ratio:\n", - "\n", - "\\[\n", - "3 + 4 + 2 = 9 \\text{ parts}\n", - "\\]\n", - "\n", - "Next, we calculate the value of one part by dividing the total number of cookies by the total number of parts:\n", - "\n", - "\\[\n", - "\\text{Value of one part} = \\frac{432}{9} = 48\n", - "\\]\n", - "\n", - "Now, we can find out how many cookies each person receives by multiplying the value of one part by the number of parts each person receives:\n", - "\n", - "- For Alice (3 parts):\n", - "\\[\n", - "3 \\times 48 = 144 \\text{ cookies}\n", - "\\]\n", - "\n", - "- For Bob (4 parts):\n", - "\\[\n", - "4 \\times 48 = 192 \\text{ cookies}\n", - "\\]\n", - "\n", - "- For Charlie (2 parts):\n", - "\\[\n", - "2 \\times 48 = 96 \\text{ cookies}\n", - "\\]\n", - "\n", - "Thus, the number of cookies each person gets is:\n", - "- **Alice**: 144 cookies\n", - "- **Bob**: 192 cookies\n", - "- **Charlie**: 96 cookies\n", - "--------------------------------------------------------------------------------\n", - "Worker-worker:default/layer_0/worker_2:\n", - "To divide the cookies in the ratio of 3:4:2, we first need to find the total parts in the ratio. \n", - "\n", - "The total parts are:\n", - "- Alice: 3 parts\n", - "- Bob: 4 parts\n", - "- Charlie: 2 parts\n", - "\n", - "Adding these parts together gives:\n", - "\\[ 3 + 4 + 2 = 9 \\text{ parts} \\]\n", - "\n", - "Next, we can determine how many cookies each part represents by dividing the total number of cookies by the total parts:\n", - "\\[ \\text{Cookies per part} = \\frac{432 \\text{ cookies}}{9 \\text{ parts}} = 48 \\text{ cookies/part} \\]\n", - "\n", - "Now we can calculate the number of cookies for each person:\n", - "- Alice's share: \n", - "\\[ 3 \\text{ parts} \\times 48 \\text{ cookies/part} = 144 \\text{ cookies} \\]\n", - "- Bob's share: \n", - "\\[ 4 \\text{ parts} \\times 48 \\text{ cookies/part} = 192 \\text{ cookies} \\]\n", - "- Charlie's share: \n", - "\\[ 2 \\text{ parts} \\times 48 \\text{ cookies/part} = 96 \\text{ cookies} \\]\n", - "\n", - "So, the final distribution of cookies is:\n", - "- Alice: 144 cookies\n", - "- Bob: 192 cookies\n", - "- Charlie: 96 cookies\n", - "--------------------------------------------------------------------------------\n", - "Orchestrator-orchestrator:default:\n", - "Received results from workers at layer 0\n", - "--------------------------------------------------------------------------------\n", - "Orchestrator-orchestrator:default:\n", - "Dispatch to workers at layer 1\n", - "--------------------------------------------------------------------------------\n", - "Worker-worker:default/layer_1/worker_2:\n", - "To divide 432 cookies in the ratio of 3:4:2 among Alice, Bob, and Charlie, follow these steps:\n", - "\n", - "1. **Determine the total number of parts in the ratio**:\n", - " \\[\n", - " 3 + 4 + 2 = 9 \\text{ parts}\n", - " \\]\n", - "\n", - "2. **Calculate the value of one part** by dividing the total number of cookies by the total number of parts:\n", - " \\[\n", - " \\text{Value of one part} = \\frac{432}{9} = 48\n", - " \\]\n", - "\n", - "3. **Calculate the number of cookies each person receives** by multiplying the value of one part by the number of parts each individual gets:\n", - " - **For Alice (3 parts)**:\n", - " \\[\n", - " 3 \\times 48 = 144 \\text{ cookies}\n", - " \\]\n", - " - **For Bob (4 parts)**:\n", - " \\[\n", - " 4 \\times 48 = 192 \\text{ cookies}\n", - " \\]\n", - " - **For Charlie (2 parts)**:\n", - " \\[\n", - " 2 \\times 48 = 96 \\text{ cookies}\n", - " \\]\n", - "\n", - "Thus, the final distribution of cookies is:\n", - "- **Alice**: 144 cookies\n", - "- **Bob**: 192 cookies\n", - "- **Charlie**: 96 cookies\n", - "--------------------------------------------------------------------------------\n", - "Worker-worker:default/layer_1/worker_0:\n", - "To divide 432 cookies among Alice, Bob, and Charlie in the ratio of 3:4:2, we can follow these steps:\n", - "\n", - "1. **Calculate the Total Parts**: \n", - " Add the parts of the ratio together:\n", - " \\[\n", - " 3 + 4 + 2 = 9 \\text{ parts}\n", - " \\]\n", - "\n", - "2. **Determine the Value of One Part**: \n", - " Divide the total number of cookies by the total number of parts:\n", - " \\[\n", - " \\text{Value of one part} = \\frac{432 \\text{ cookies}}{9 \\text{ parts}} = 48 \\text{ cookies/part}\n", - " \\]\n", - "\n", - "3. **Calculate Each Person's Share**:\n", - " - **Alice's Share** (3 parts):\n", - " \\[\n", - " 3 \\times 48 = 144 \\text{ cookies}\n", - " \\]\n", - " - **Bob's Share** (4 parts):\n", - " \\[\n", - " 4 \\times 48 = 192 \\text{ cookies}\n", - " \\]\n", - " - **Charlie's Share** (2 parts):\n", - " \\[\n", - " 2 \\times 48 = 96 \\text{ cookies}\n", - " \\]\n", - "\n", - "4. **Final Distribution**:\n", - " - Alice: 144 cookies\n", - " - Bob: 192 cookies\n", - " - Charlie: 96 cookies\n", - "\n", - "Thus, the distribution of cookies is:\n", - "- **Alice**: 144 cookies\n", - "- **Bob**: 192 cookies\n", - "- **Charlie**: 96 cookies\n", - "--------------------------------------------------------------------------------\n", - "Worker-worker:default/layer_1/worker_1:\n", - "To divide 432 cookies among Alice, Bob, and Charlie in the ratio of 3:4:2, we first need to determine the total number of parts in this ratio.\n", - "\n", - "1. **Calculate Total Parts:**\n", - " \\[\n", - " 3 \\text{ (Alice)} + 4 \\text{ (Bob)} + 2 \\text{ (Charlie)} = 9 \\text{ parts}\n", - " \\]\n", - "\n", - "2. **Determine the Value of One Part:**\n", - " Next, we'll find out how many cookies correspond to one part by dividing the total number of cookies by the total number of parts:\n", - " \\[\n", - " \\text{Value of one part} = \\frac{432 \\text{ cookies}}{9 \\text{ parts}} = 48 \\text{ cookies/part}\n", - " \\]\n", - "\n", - "3. **Calculate the Share for Each Person:**\n", - " - **Alice's Share (3 parts):**\n", - " \\[\n", - " 3 \\times 48 = 144 \\text{ cookies}\n", - " \\]\n", - " - **Bob's Share (4 parts):**\n", - " \\[\n", - " 4 \\times 48 = 192 \\text{ cookies}\n", - " \\]\n", - " - **Charlie’s Share (2 parts):**\n", - " \\[\n", - " 2 \\times 48 = 96 \\text{ cookies}\n", - " \\]\n", - "\n", - "4. **Summary of the Distribution:**\n", - " - **Alice:** 144 cookies\n", - " - **Bob:** 192 cookies\n", - " - **Charlie:** 96 cookies\n", - "\n", - "In conclusion, Alice receives 144 cookies, Bob receives 192 cookies, and Charlie receives 96 cookies.\n", - "--------------------------------------------------------------------------------\n", - "Orchestrator-orchestrator:default:\n", - "Received results from workers at layer 1\n", - "--------------------------------------------------------------------------------\n", - "Orchestrator-orchestrator:default:\n", - "Performing final aggregation\n", - "--------------------------------------------------------------------------------\n", - "Final result:\n", - "To divide 432 cookies among Alice, Bob, and Charlie in the ratio of 3:4:2, follow these steps:\n", - "\n", - "1. **Calculate the Total Parts in the Ratio:**\n", - " Add the parts of the ratio together:\n", - " \\[\n", - " 3 + 4 + 2 = 9\n", - " \\]\n", - "\n", - "2. **Determine the Value of One Part:**\n", - " Divide the total number of cookies by the total number of parts:\n", - " \\[\n", - " \\text{Value of one part} = \\frac{432}{9} = 48 \\text{ cookies/part}\n", - " \\]\n", - "\n", - "3. **Calculate Each Person's Share:**\n", - " - **Alice's Share (3 parts):**\n", - " \\[\n", - " 3 \\times 48 = 144 \\text{ cookies}\n", - " \\]\n", - " - **Bob's Share (4 parts):**\n", - " \\[\n", - " 4 \\times 48 = 192 \\text{ cookies}\n", - " \\]\n", - " - **Charlie's Share (2 parts):**\n", - " \\[\n", - " 2 \\times 48 = 96 \\text{ cookies}\n", - " \\]\n", - "\n", - "Therefore, the distribution of cookies is as follows:\n", - "- **Alice:** 144 cookies\n", - "- **Bob:** 192 cookies\n", - "- **Charlie:** 96 cookies\n", - "\n", - "In summary, Alice gets 144 cookies, Bob gets 192 cookies, and Charlie gets 96 cookies.\n" - ] - } - ], - "source": [ - "runtime = SingleThreadedAgentRuntime()\n", - "await WorkerAgent.register(\n", - " runtime, \"worker\", lambda: WorkerAgent(model_client=OpenAIChatCompletionClient(model=\"gpt-4o-mini\"))\n", - ")\n", - "await OrchestratorAgent.register(\n", - " runtime,\n", - " \"orchestrator\",\n", - " lambda: OrchestratorAgent(\n", - " model_client=OpenAIChatCompletionClient(model=\"gpt-4o\"), worker_agent_types=[\"worker\"] * 3, num_layers=3\n", - " ),\n", - ")\n", - "\n", - "runtime.start()\n", - "result = await runtime.send_message(UserTask(task=task), AgentId(\"orchestrator\", \"default\"))\n", - "await runtime.stop_when_idle()\n", - "print(f\"{'-'*80}\\nFinal result:\\n{result.result}\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } - }, - "nbformat": 4, - "nbformat_minor": 2 + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Mixture of Agents\n", + "\n", + "[Mixture of Agents](https://arxiv.org/abs/2406.04692) is a multi-agent design pattern\n", + "that models after the feed-forward neural network architecture.\n", + "\n", + "The pattern consists of two types of agents: worker agents and a single orchestrator agent.\n", + "Worker agents are organized into multiple layers, with each layer consisting of a fixed number of worker agents.\n", + "Messages from the worker agents in a previous layer are concatenated and sent to\n", + "all the worker agents in the next layer.\n", + "\n", + "This example implements the Mixture of Agents pattern using the core library\n", + "following the [original implementation](https://github.com/togethercomputer/moa) of multi-layer mixture of agents.\n", + "\n", + "Here is a high-level procedure overview of the pattern:\n", + "1. The orchestrator agent takes input a user task and first dispatches it to the worker agents in the first layer.\n", + "2. The worker agents in the first layer process the task and return the results to the orchestrator agent.\n", + "3. The orchestrator agent then synthesizes the results from the first layer and dispatches an updated task with the previous results to the worker agents in the second layer.\n", + "4. The process continues until the final layer is reached.\n", + "5. In the final layer, the orchestrator agent aggregates the results from previous layer and returns a single final result to the user.\n", + "\n", + "We use the direct messaging API {py:meth}`~autogen_core.base.BaseAgent.send_message` to implement this pattern.\n", + "This makes it easier to add more features like worker task cancellation and error handling in the future." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import asyncio\n", + "from dataclasses import dataclass\n", + "from typing import List\n", + "\n", + "from autogen_core import AgentId, MessageContext, RoutedAgent, message_handler\n", + "from autogen_core.application import SingleThreadedAgentRuntime\n", + "from autogen_core.components.models import ChatCompletionClient, SystemMessage, UserMessage\n", + "from autogen_ext.models import OpenAIChatCompletionClient" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Message Protocol\n", + "\n", + "The agents communicate using the following messages:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "@dataclass\n", + "class WorkerTask:\n", + " task: str\n", + " previous_results: List[str]\n", + "\n", + "\n", + "@dataclass\n", + "class WorkerTaskResult:\n", + " result: str\n", + "\n", + "\n", + "@dataclass\n", + "class UserTask:\n", + " task: str\n", + "\n", + "\n", + "@dataclass\n", + "class FinalResult:\n", + " result: str" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Worker Agent\n", + "\n", + "Each worker agent receives a task from the orchestrator agent and processes them\n", + "indepedently.\n", + "Once the task is completed, the worker agent returns the result." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "class WorkerAgent(RoutedAgent):\n", + " def __init__(\n", + " self,\n", + " model_client: ChatCompletionClient,\n", + " ) -> None:\n", + " super().__init__(description=\"Worker Agent\")\n", + " self._model_client = model_client\n", + "\n", + " @message_handler\n", + " async def handle_task(self, message: WorkerTask, ctx: MessageContext) -> WorkerTaskResult:\n", + " if message.previous_results:\n", + " # If previous results are provided, we need to synthesize them to create a single prompt.\n", + " system_prompt = \"You have been provided with a set of responses from various open-source models to the latest user query. Your task is to synthesize these responses into a single, high-quality response. It is crucial to critically evaluate the information provided in these responses, recognizing that some of it may be biased or incorrect. Your response should not simply replicate the given answers but should offer a refined, accurate, and comprehensive reply to the instruction. Ensure your response is well-structured, coherent, and adheres to the highest standards of accuracy and reliability.\\n\\nResponses from models:\"\n", + " system_prompt += \"\\n\" + \"\\n\\n\".join([f\"{i+1}. {r}\" for i, r in enumerate(message.previous_results)])\n", + " model_result = await self._model_client.create(\n", + " [SystemMessage(content=system_prompt), UserMessage(content=message.task, source=\"user\")]\n", + " )\n", + " else:\n", + " # If no previous results are provided, we can simply pass the user query to the model.\n", + " model_result = await self._model_client.create([UserMessage(content=message.task, source=\"user\")])\n", + " assert isinstance(model_result.content, str)\n", + " print(f\"{'-'*80}\\nWorker-{self.id}:\\n{model_result.content}\")\n", + " return WorkerTaskResult(result=model_result.content)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Orchestrator Agent\n", + "\n", + "The orchestrator agent receives tasks from the user and distributes them to the worker agents,\n", + "iterating over multiple layers of worker agents. Once all worker agents have processed the task,\n", + "the orchestrator agent aggregates the results and publishes the final result." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "class OrchestratorAgent(RoutedAgent):\n", + " def __init__(\n", + " self,\n", + " model_client: ChatCompletionClient,\n", + " worker_agent_types: List[str],\n", + " num_layers: int,\n", + " ) -> None:\n", + " super().__init__(description=\"Aggregator Agent\")\n", + " self._model_client = model_client\n", + " self._worker_agent_types = worker_agent_types\n", + " self._num_layers = num_layers\n", + "\n", + " @message_handler\n", + " async def handle_task(self, message: UserTask, ctx: MessageContext) -> FinalResult:\n", + " print(f\"{'-'*80}\\nOrchestrator-{self.id}:\\nReceived task: {message.task}\")\n", + " # Create task for the first layer.\n", + " worker_task = WorkerTask(task=message.task, previous_results=[])\n", + " # Iterate over layers.\n", + " for i in range(self._num_layers - 1):\n", + " # Assign workers for this layer.\n", + " worker_ids = [\n", + " AgentId(worker_type, f\"{self.id.key}/layer_{i}/worker_{j}\")\n", + " for j, worker_type in enumerate(self._worker_agent_types)\n", + " ]\n", + " # Dispatch tasks to workers.\n", + " print(f\"{'-'*80}\\nOrchestrator-{self.id}:\\nDispatch to workers at layer {i}\")\n", + " results = await asyncio.gather(*[self.send_message(worker_task, worker_id) for worker_id in worker_ids])\n", + " print(f\"{'-'*80}\\nOrchestrator-{self.id}:\\nReceived results from workers at layer {i}\")\n", + " # Prepare task for the next layer.\n", + " worker_task = WorkerTask(task=message.task, previous_results=[r.result for r in results])\n", + " # Perform final aggregation.\n", + " print(f\"{'-'*80}\\nOrchestrator-{self.id}:\\nPerforming final aggregation\")\n", + " system_prompt = \"You have been provided with a set of responses from various open-source models to the latest user query. Your task is to synthesize these responses into a single, high-quality response. It is crucial to critically evaluate the information provided in these responses, recognizing that some of it may be biased or incorrect. Your response should not simply replicate the given answers but should offer a refined, accurate, and comprehensive reply to the instruction. Ensure your response is well-structured, coherent, and adheres to the highest standards of accuracy and reliability.\\n\\nResponses from models:\"\n", + " system_prompt += \"\\n\" + \"\\n\\n\".join([f\"{i+1}. {r}\" for i, r in enumerate(worker_task.previous_results)])\n", + " model_result = await self._model_client.create(\n", + " [SystemMessage(content=system_prompt), UserMessage(content=message.task, source=\"user\")]\n", + " )\n", + " assert isinstance(model_result.content, str)\n", + " return FinalResult(result=model_result.content)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Running Mixture of Agents\n", + "\n", + "Let's run the mixture of agents on a math task. You can change the task to make it more challenging, for example, by trying tasks from the [International Mathematical Olympiad](https://www.imo-official.org/problems.aspx)." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "task = (\n", + " \"I have 432 cookies, and divide them 3:4:2 between Alice, Bob, and Charlie. How many cookies does each person get?\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's set up the runtime with 3 layers of worker agents, each layer consisting of 3 worker agents.\n", + "We only need to register a single worker agent types, \"worker\", because we are using\n", + "the same model client configuration (i.e., gpt-4o-mini) for all worker agents.\n", + "If you want to use different models, you will need to register multiple worker agent types,\n", + "one for each model, and update the `worker_agent_types` list in the orchestrator agent's\n", + "factory function.\n", + "\n", + "The instances of worker agents are automatically created when the orchestrator agent\n", + "dispatches tasks to them.\n", + "See [Agent Identity and Lifecycle](../core-concepts/agent-identity-and-lifecycle.md)\n", + "for more information on agent lifecycle." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--------------------------------------------------------------------------------\n", + "Orchestrator-orchestrator:default:\n", + "Received task: I have 432 cookies, and divide them 3:4:2 between Alice, Bob, and Charlie. How many cookies does each person get?\n", + "--------------------------------------------------------------------------------\n", + "Orchestrator-orchestrator:default:\n", + "Dispatch to workers at layer 0\n", + "--------------------------------------------------------------------------------\n", + "Worker-worker:default/layer_0/worker_1:\n", + "To divide 432 cookies in the ratio of 3:4:2 between Alice, Bob, and Charlie, you first need to determine the total number of parts in the ratio.\n", + "\n", + "Add the parts together:\n", + "\\[ 3 + 4 + 2 = 9 \\]\n", + "\n", + "Now, you can find the value of one part by dividing the total number of cookies by the total number of parts:\n", + "\\[ \\text{Value of one part} = \\frac{432}{9} = 48 \\]\n", + "\n", + "Now, multiply the value of one part by the number of parts for each person:\n", + "\n", + "- For Alice (3 parts):\n", + "\\[ 3 \\times 48 = 144 \\]\n", + "\n", + "- For Bob (4 parts):\n", + "\\[ 4 \\times 48 = 192 \\]\n", + "\n", + "- For Charlie (2 parts):\n", + "\\[ 2 \\times 48 = 96 \\]\n", + "\n", + "Thus, the number of cookies each person gets is:\n", + "- Alice: 144 cookies\n", + "- Bob: 192 cookies\n", + "- Charlie: 96 cookies\n", + "--------------------------------------------------------------------------------\n", + "Worker-worker:default/layer_0/worker_0:\n", + "To divide 432 cookies in the ratio of 3:4:2 between Alice, Bob, and Charlie, we will first determine the total number of parts in the ratio:\n", + "\n", + "\\[\n", + "3 + 4 + 2 = 9 \\text{ parts}\n", + "\\]\n", + "\n", + "Next, we calculate the value of one part by dividing the total number of cookies by the total number of parts:\n", + "\n", + "\\[\n", + "\\text{Value of one part} = \\frac{432}{9} = 48\n", + "\\]\n", + "\n", + "Now, we can find out how many cookies each person receives by multiplying the value of one part by the number of parts each person receives:\n", + "\n", + "- For Alice (3 parts):\n", + "\\[\n", + "3 \\times 48 = 144 \\text{ cookies}\n", + "\\]\n", + "\n", + "- For Bob (4 parts):\n", + "\\[\n", + "4 \\times 48 = 192 \\text{ cookies}\n", + "\\]\n", + "\n", + "- For Charlie (2 parts):\n", + "\\[\n", + "2 \\times 48 = 96 \\text{ cookies}\n", + "\\]\n", + "\n", + "Thus, the number of cookies each person gets is:\n", + "- **Alice**: 144 cookies\n", + "- **Bob**: 192 cookies\n", + "- **Charlie**: 96 cookies\n", + "--------------------------------------------------------------------------------\n", + "Worker-worker:default/layer_0/worker_2:\n", + "To divide the cookies in the ratio of 3:4:2, we first need to find the total parts in the ratio. \n", + "\n", + "The total parts are:\n", + "- Alice: 3 parts\n", + "- Bob: 4 parts\n", + "- Charlie: 2 parts\n", + "\n", + "Adding these parts together gives:\n", + "\\[ 3 + 4 + 2 = 9 \\text{ parts} \\]\n", + "\n", + "Next, we can determine how many cookies each part represents by dividing the total number of cookies by the total parts:\n", + "\\[ \\text{Cookies per part} = \\frac{432 \\text{ cookies}}{9 \\text{ parts}} = 48 \\text{ cookies/part} \\]\n", + "\n", + "Now we can calculate the number of cookies for each person:\n", + "- Alice's share: \n", + "\\[ 3 \\text{ parts} \\times 48 \\text{ cookies/part} = 144 \\text{ cookies} \\]\n", + "- Bob's share: \n", + "\\[ 4 \\text{ parts} \\times 48 \\text{ cookies/part} = 192 \\text{ cookies} \\]\n", + "- Charlie's share: \n", + "\\[ 2 \\text{ parts} \\times 48 \\text{ cookies/part} = 96 \\text{ cookies} \\]\n", + "\n", + "So, the final distribution of cookies is:\n", + "- Alice: 144 cookies\n", + "- Bob: 192 cookies\n", + "- Charlie: 96 cookies\n", + "--------------------------------------------------------------------------------\n", + "Orchestrator-orchestrator:default:\n", + "Received results from workers at layer 0\n", + "--------------------------------------------------------------------------------\n", + "Orchestrator-orchestrator:default:\n", + "Dispatch to workers at layer 1\n", + "--------------------------------------------------------------------------------\n", + "Worker-worker:default/layer_1/worker_2:\n", + "To divide 432 cookies in the ratio of 3:4:2 among Alice, Bob, and Charlie, follow these steps:\n", + "\n", + "1. **Determine the total number of parts in the ratio**:\n", + " \\[\n", + " 3 + 4 + 2 = 9 \\text{ parts}\n", + " \\]\n", + "\n", + "2. **Calculate the value of one part** by dividing the total number of cookies by the total number of parts:\n", + " \\[\n", + " \\text{Value of one part} = \\frac{432}{9} = 48\n", + " \\]\n", + "\n", + "3. **Calculate the number of cookies each person receives** by multiplying the value of one part by the number of parts each individual gets:\n", + " - **For Alice (3 parts)**:\n", + " \\[\n", + " 3 \\times 48 = 144 \\text{ cookies}\n", + " \\]\n", + " - **For Bob (4 parts)**:\n", + " \\[\n", + " 4 \\times 48 = 192 \\text{ cookies}\n", + " \\]\n", + " - **For Charlie (2 parts)**:\n", + " \\[\n", + " 2 \\times 48 = 96 \\text{ cookies}\n", + " \\]\n", + "\n", + "Thus, the final distribution of cookies is:\n", + "- **Alice**: 144 cookies\n", + "- **Bob**: 192 cookies\n", + "- **Charlie**: 96 cookies\n", + "--------------------------------------------------------------------------------\n", + "Worker-worker:default/layer_1/worker_0:\n", + "To divide 432 cookies among Alice, Bob, and Charlie in the ratio of 3:4:2, we can follow these steps:\n", + "\n", + "1. **Calculate the Total Parts**: \n", + " Add the parts of the ratio together:\n", + " \\[\n", + " 3 + 4 + 2 = 9 \\text{ parts}\n", + " \\]\n", + "\n", + "2. **Determine the Value of One Part**: \n", + " Divide the total number of cookies by the total number of parts:\n", + " \\[\n", + " \\text{Value of one part} = \\frac{432 \\text{ cookies}}{9 \\text{ parts}} = 48 \\text{ cookies/part}\n", + " \\]\n", + "\n", + "3. **Calculate Each Person's Share**:\n", + " - **Alice's Share** (3 parts):\n", + " \\[\n", + " 3 \\times 48 = 144 \\text{ cookies}\n", + " \\]\n", + " - **Bob's Share** (4 parts):\n", + " \\[\n", + " 4 \\times 48 = 192 \\text{ cookies}\n", + " \\]\n", + " - **Charlie's Share** (2 parts):\n", + " \\[\n", + " 2 \\times 48 = 96 \\text{ cookies}\n", + " \\]\n", + "\n", + "4. **Final Distribution**:\n", + " - Alice: 144 cookies\n", + " - Bob: 192 cookies\n", + " - Charlie: 96 cookies\n", + "\n", + "Thus, the distribution of cookies is:\n", + "- **Alice**: 144 cookies\n", + "- **Bob**: 192 cookies\n", + "- **Charlie**: 96 cookies\n", + "--------------------------------------------------------------------------------\n", + "Worker-worker:default/layer_1/worker_1:\n", + "To divide 432 cookies among Alice, Bob, and Charlie in the ratio of 3:4:2, we first need to determine the total number of parts in this ratio.\n", + "\n", + "1. **Calculate Total Parts:**\n", + " \\[\n", + " 3 \\text{ (Alice)} + 4 \\text{ (Bob)} + 2 \\text{ (Charlie)} = 9 \\text{ parts}\n", + " \\]\n", + "\n", + "2. **Determine the Value of One Part:**\n", + " Next, we'll find out how many cookies correspond to one part by dividing the total number of cookies by the total number of parts:\n", + " \\[\n", + " \\text{Value of one part} = \\frac{432 \\text{ cookies}}{9 \\text{ parts}} = 48 \\text{ cookies/part}\n", + " \\]\n", + "\n", + "3. **Calculate the Share for Each Person:**\n", + " - **Alice's Share (3 parts):**\n", + " \\[\n", + " 3 \\times 48 = 144 \\text{ cookies}\n", + " \\]\n", + " - **Bob's Share (4 parts):**\n", + " \\[\n", + " 4 \\times 48 = 192 \\text{ cookies}\n", + " \\]\n", + " - **Charlie’s Share (2 parts):**\n", + " \\[\n", + " 2 \\times 48 = 96 \\text{ cookies}\n", + " \\]\n", + "\n", + "4. **Summary of the Distribution:**\n", + " - **Alice:** 144 cookies\n", + " - **Bob:** 192 cookies\n", + " - **Charlie:** 96 cookies\n", + "\n", + "In conclusion, Alice receives 144 cookies, Bob receives 192 cookies, and Charlie receives 96 cookies.\n", + "--------------------------------------------------------------------------------\n", + "Orchestrator-orchestrator:default:\n", + "Received results from workers at layer 1\n", + "--------------------------------------------------------------------------------\n", + "Orchestrator-orchestrator:default:\n", + "Performing final aggregation\n", + "--------------------------------------------------------------------------------\n", + "Final result:\n", + "To divide 432 cookies among Alice, Bob, and Charlie in the ratio of 3:4:2, follow these steps:\n", + "\n", + "1. **Calculate the Total Parts in the Ratio:**\n", + " Add the parts of the ratio together:\n", + " \\[\n", + " 3 + 4 + 2 = 9\n", + " \\]\n", + "\n", + "2. **Determine the Value of One Part:**\n", + " Divide the total number of cookies by the total number of parts:\n", + " \\[\n", + " \\text{Value of one part} = \\frac{432}{9} = 48 \\text{ cookies/part}\n", + " \\]\n", + "\n", + "3. **Calculate Each Person's Share:**\n", + " - **Alice's Share (3 parts):**\n", + " \\[\n", + " 3 \\times 48 = 144 \\text{ cookies}\n", + " \\]\n", + " - **Bob's Share (4 parts):**\n", + " \\[\n", + " 4 \\times 48 = 192 \\text{ cookies}\n", + " \\]\n", + " - **Charlie's Share (2 parts):**\n", + " \\[\n", + " 2 \\times 48 = 96 \\text{ cookies}\n", + " \\]\n", + "\n", + "Therefore, the distribution of cookies is as follows:\n", + "- **Alice:** 144 cookies\n", + "- **Bob:** 192 cookies\n", + "- **Charlie:** 96 cookies\n", + "\n", + "In summary, Alice gets 144 cookies, Bob gets 192 cookies, and Charlie gets 96 cookies.\n" + ] + } + ], + "source": [ + "runtime = SingleThreadedAgentRuntime()\n", + "await WorkerAgent.register(\n", + " runtime, \"worker\", lambda: WorkerAgent(model_client=OpenAIChatCompletionClient(model=\"gpt-4o-mini\"))\n", + ")\n", + "await OrchestratorAgent.register(\n", + " runtime,\n", + " \"orchestrator\",\n", + " lambda: OrchestratorAgent(\n", + " model_client=OpenAIChatCompletionClient(model=\"gpt-4o\"), worker_agent_types=[\"worker\"] * 3, num_layers=3\n", + " ),\n", + ")\n", + "\n", + "runtime.start()\n", + "result = await runtime.send_message(UserTask(task=task), AgentId(\"orchestrator\", \"default\"))\n", + "await runtime.stop_when_idle()\n", + "print(f\"{'-'*80}\\nFinal result:\\n{result.result}\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/multi-agent-debate.ipynb b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/multi-agent-debate.ipynb index a737c09634b2..ad0137a462f1 100644 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/multi-agent-debate.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/multi-agent-debate.ipynb @@ -1,571 +1,571 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Multi-Agent Debate\n", - "\n", - "Multi-Agent Debate is a multi-agent design pattern that simulates a multi-turn interaction \n", - "where in each turn, agents exchange their responses with each other, and refine \n", - "their responses based on the responses from other agents.\n", - "\n", - "This example shows an implementation of the multi-agent debate pattern for solving\n", - "math problems from the [GSM8K benchmark](https://huggingface.co/datasets/openai/gsm8k).\n", - "\n", - "There are of two types of agents in this pattern: solver agents and an aggregator agent.\n", - "The solver agents are connected in a sparse manner following the technique described in\n", - "[Improving Multi-Agent Debate with Sparse Communication Topology](https://arxiv.org/abs/2406.11776).\n", - "The solver agents are responsible for solving math problems and exchanging responses with each other.\n", - "The aggregator agent is responsible for distributing math problems to the solver agents,\n", - "waiting for their final responses, and aggregating the responses to get the final answer.\n", - "\n", - "The pattern works as follows:\n", - "1. User sends a math problem to the aggregator agent.\n", - "2. The aggregator agent distributes the problem to the solver agents.\n", - "3. Each solver agent processes the problem, and publishes a response to its neighbors.\n", - "4. Each solver agent uses the responses from its neighbors to refine its response, and publishes a new response.\n", - "5. Repeat step 4 for a fixed number of rounds. In the final round, each solver agent publishes a final response.\n", - "6. The aggregator agent uses majority voting to aggregate the final responses from all solver agents to get a final answer, and publishes the answer.\n", - "\n", - "We will be using the broadcast API, i.e., {py:meth}`~autogen_core.base.BaseAgent.publish_message`,\n", - "and we will be using topic and subscription to implement the communication topology.\n", - "Read about [Topics and Subscriptions](../core-concepts/topic-and-subscription.md) to understand how they work." - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "metadata": {}, - "outputs": [], - "source": [ - "import re\n", - "from dataclasses import dataclass\n", - "from typing import Dict, List\n", - "\n", - "from autogen_core import (\n", - " DefaultTopicId,\n", - " MessageContext,\n", - " RoutedAgent,\n", - " TypeSubscription,\n", - " default_subscription,\n", - " message_handler,\n", - ")\n", - "from autogen_core.application import SingleThreadedAgentRuntime\n", - "from autogen_core.components.models import (\n", - " AssistantMessage,\n", - " ChatCompletionClient,\n", - " LLMMessage,\n", - " SystemMessage,\n", - " UserMessage,\n", - ")\n", - "from autogen_ext.models import OpenAIChatCompletionClient" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Message Protocol\n", - "\n", - "First, we define the messages used by the agents.\n", - "`IntermediateSolverResponse` is the message exchanged among the solver agents in each round,\n", - "and `FinalSolverResponse` is the message published by the solver agents in the final round." - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "metadata": {}, - "outputs": [], - "source": [ - "@dataclass\n", - "class Question:\n", - " content: str\n", - "\n", - "\n", - "@dataclass\n", - "class Answer:\n", - " content: str\n", - "\n", - "\n", - "@dataclass\n", - "class SolverRequest:\n", - " content: str\n", - " question: str\n", - "\n", - "\n", - "@dataclass\n", - "class IntermediateSolverResponse:\n", - " content: str\n", - " question: str\n", - " answer: str\n", - " round: int\n", - "\n", - "\n", - "@dataclass\n", - "class FinalSolverResponse:\n", - " answer: str" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Solver Agent\n", - "\n", - "The solver agent is responsible for solving math problems and exchanging responses with other solver agents.\n", - "Upon receiving a `SolverRequest`, the solver agent uses an LLM to generate an answer.\n", - "Then, it publishes a `IntermediateSolverResponse`\n", - "or a `FinalSolverResponse` based on the round number.\n", - "\n", - "The solver agent is given a topic type, which is used to indicate the topic\n", - "to which the agent should publish intermediate responses. This topic is subscribed\n", - "to by its neighbors to receive responses from this agent -- we will show\n", - "how this is done later.\n", - "\n", - "We use {py:meth}`~autogen_core.components.default_subscription` to let\n", - "solver agents subscribe to the default topic, which is used by the aggregator agent\n", - "to collect the final responses from the solver agents." - ] - }, - { - "cell_type": "code", - "execution_count": 40, - "metadata": {}, - "outputs": [], - "source": [ - "@default_subscription\n", - "class MathSolver(RoutedAgent):\n", - " def __init__(self, model_client: ChatCompletionClient, topic_type: str, num_neighbors: int, max_round: int) -> None:\n", - " super().__init__(\"A debator.\")\n", - " self._topic_type = topic_type\n", - " self._model_client = model_client\n", - " self._num_neighbors = num_neighbors\n", - " self._history: List[LLMMessage] = []\n", - " self._buffer: Dict[int, List[IntermediateSolverResponse]] = {}\n", - " self._system_messages = [\n", - " SystemMessage(\n", - " (\n", - " \"You are a helpful assistant with expertise in mathematics and reasoning. \"\n", - " \"Your task is to assist in solving a math reasoning problem by providing \"\n", - " \"a clear and detailed solution. Limit your output within 100 words, \"\n", - " \"and your final answer should be a single numerical number, \"\n", - " \"in the form of {{answer}}, at the end of your response. \"\n", - " \"For example, 'The answer is {{42}}.'\"\n", - " )\n", - " )\n", - " ]\n", - " self._round = 0\n", - " self._max_round = max_round\n", - "\n", - " @message_handler\n", - " async def handle_request(self, message: SolverRequest, ctx: MessageContext) -> None:\n", - " # Add the question to the memory.\n", - " self._history.append(UserMessage(content=message.content, source=\"user\"))\n", - " # Make an inference using the model.\n", - " model_result = await self._model_client.create(self._system_messages + self._history)\n", - " assert isinstance(model_result.content, str)\n", - " # Add the response to the memory.\n", - " self._history.append(AssistantMessage(content=model_result.content, source=self.metadata[\"type\"]))\n", - " print(f\"{'-'*80}\\nSolver {self.id} round {self._round}:\\n{model_result.content}\")\n", - " # Extract the answer from the response.\n", - " match = re.search(r\"\\{\\{(\\-?\\d+(\\.\\d+)?)\\}\\}\", model_result.content)\n", - " if match is None:\n", - " raise ValueError(\"The model response does not contain the answer.\")\n", - " answer = match.group(1)\n", - " # Increment the counter.\n", - " self._round += 1\n", - " if self._round == self._max_round:\n", - " # If the counter reaches the maximum round, publishes a final response.\n", - " await self.publish_message(FinalSolverResponse(answer=answer), topic_id=DefaultTopicId())\n", - " else:\n", - " # Publish intermediate response to the topic associated with this solver.\n", - " await self.publish_message(\n", - " IntermediateSolverResponse(\n", - " content=model_result.content,\n", - " question=message.question,\n", - " answer=answer,\n", - " round=self._round,\n", - " ),\n", - " topic_id=DefaultTopicId(type=self._topic_type),\n", - " )\n", - "\n", - " @message_handler\n", - " async def handle_response(self, message: IntermediateSolverResponse, ctx: MessageContext) -> None:\n", - " # Add neighbor's response to the buffer.\n", - " self._buffer.setdefault(message.round, []).append(message)\n", - " # Check if all neighbors have responded.\n", - " if len(self._buffer[message.round]) == self._num_neighbors:\n", - " print(\n", - " f\"{'-'*80}\\nSolver {self.id} round {message.round}:\\nReceived all responses from {self._num_neighbors} neighbors.\"\n", - " )\n", - " # Prepare the prompt for the next question.\n", - " prompt = \"These are the solutions to the problem from other agents:\\n\"\n", - " for resp in self._buffer[message.round]:\n", - " prompt += f\"One agent solution: {resp.content}\\n\"\n", - " prompt += (\n", - " \"Using the solutions from other agents as additional information, \"\n", - " \"can you provide your answer to the math problem? \"\n", - " f\"The original math problem is {message.question}. \"\n", - " \"Your final answer should be a single numerical number, \"\n", - " \"in the form of {{answer}}, at the end of your response.\"\n", - " )\n", - " # Send the question to the agent itself to solve.\n", - " await self.send_message(SolverRequest(content=prompt, question=message.question), self.id)\n", - " # Clear the buffer.\n", - " self._buffer.pop(message.round)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Aggregator Agent\n", - "\n", - "The aggregator agent is responsible for handling user question and \n", - "distributing math problems to the solver agents.\n", - "\n", - "The aggregator subscribes to the default topic using\n", - "{py:meth}`~autogen_core.components.default_subscription`. The default topic is used to\n", - "recieve user question, receive the final responses from the solver agents,\n", - "and publish the final answer back to the user.\n", - "\n", - "In a more complex application when you want to isolate the multi-agent debate into a\n", - "sub-component, you should use\n", - "{py:meth}`~autogen_core.components.type_subscription` to set a specific topic\n", - "type for the aggregator-solver communication, \n", - "and have the both the solver and aggregator publish and subscribe to that topic type." - ] - }, - { - "cell_type": "code", - "execution_count": 41, - "metadata": {}, - "outputs": [], - "source": [ - "@default_subscription\n", - "class MathAggregator(RoutedAgent):\n", - " def __init__(self, num_solvers: int) -> None:\n", - " super().__init__(\"Math Aggregator\")\n", - " self._num_solvers = num_solvers\n", - " self._buffer: List[FinalSolverResponse] = []\n", - "\n", - " @message_handler\n", - " async def handle_question(self, message: Question, ctx: MessageContext) -> None:\n", - " print(f\"{'-'*80}\\nAggregator {self.id} received question:\\n{message.content}\")\n", - " prompt = (\n", - " f\"Can you solve the following math problem?\\n{message.content}\\n\"\n", - " \"Explain your reasoning. Your final answer should be a single numerical number, \"\n", - " \"in the form of {{answer}}, at the end of your response.\"\n", - " )\n", - " print(f\"{'-'*80}\\nAggregator {self.id} publishes initial solver request.\")\n", - " await self.publish_message(SolverRequest(content=prompt, question=message.content), topic_id=DefaultTopicId())\n", - "\n", - " @message_handler\n", - " async def handle_final_solver_response(self, message: FinalSolverResponse, ctx: MessageContext) -> None:\n", - " self._buffer.append(message)\n", - " if len(self._buffer) == self._num_solvers:\n", - " print(f\"{'-'*80}\\nAggregator {self.id} received all final answers from {self._num_solvers} solvers.\")\n", - " # Find the majority answer.\n", - " answers = [resp.answer for resp in self._buffer]\n", - " majority_answer = max(set(answers), key=answers.count)\n", - " # Publish the aggregated response.\n", - " await self.publish_message(Answer(content=majority_answer), topic_id=DefaultTopicId())\n", - " # Clear the responses.\n", - " self._buffer.clear()\n", - " print(f\"{'-'*80}\\nAggregator {self.id} publishes final answer:\\n{majority_answer}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Setting Up a Debate\n", - "\n", - "We will now set up a multi-agent debate with 4 solver agents and 1 aggregator agent.\n", - "The solver agents will be connected in a sparse manner as illustrated in the figure\n", - "below:\n", - "\n", - "```\n", - "A --- B\n", - "| |\n", - "| |\n", - "C --- D\n", - "```\n", - "\n", - "Each solver agent is connected to two other solver agents. \n", - "For example, agent A is connected to agents B and C.\n", - "\n", - "Let's first create a runtime and register the agent types." - ] - }, - { - "cell_type": "code", - "execution_count": 42, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "AgentType(type='MathAggregator')" - ] - }, - "execution_count": 42, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "runtime = SingleThreadedAgentRuntime()\n", - "await MathSolver.register(\n", - " runtime,\n", - " \"MathSolverA\",\n", - " lambda: MathSolver(\n", - " model_client=OpenAIChatCompletionClient(model=\"gpt-4o-mini\"),\n", - " topic_type=\"MathSolverA\",\n", - " num_neighbors=2,\n", - " max_round=3,\n", - " ),\n", - ")\n", - "await MathSolver.register(\n", - " runtime,\n", - " \"MathSolverB\",\n", - " lambda: MathSolver(\n", - " model_client=OpenAIChatCompletionClient(model=\"gpt-4o-mini\"),\n", - " topic_type=\"MathSolverB\",\n", - " num_neighbors=2,\n", - " max_round=3,\n", - " ),\n", - ")\n", - "await MathSolver.register(\n", - " runtime,\n", - " \"MathSolverC\",\n", - " lambda: MathSolver(\n", - " model_client=OpenAIChatCompletionClient(model=\"gpt-4o-mini\"),\n", - " topic_type=\"MathSolverC\",\n", - " num_neighbors=2,\n", - " max_round=3,\n", - " ),\n", - ")\n", - "await MathSolver.register(\n", - " runtime,\n", - " \"MathSolverD\",\n", - " lambda: MathSolver(\n", - " model_client=OpenAIChatCompletionClient(model=\"gpt-4o-mini\"),\n", - " topic_type=\"MathSolverD\",\n", - " num_neighbors=2,\n", - " max_round=3,\n", - " ),\n", - ")\n", - "await MathAggregator.register(runtime, \"MathAggregator\", lambda: MathAggregator(num_solvers=4))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now we will create the solver agent topology using {py:class}`~autogen_core.components.TypeSubscription`,\n", - "which maps each solver agent's publishing topic type to its neighbors' agent types." - ] - }, - { - "cell_type": "code", - "execution_count": 43, - "metadata": {}, - "outputs": [], - "source": [ - "# Subscriptions for topic published to by MathSolverA.\n", - "await runtime.add_subscription(TypeSubscription(\"MathSolverA\", \"MathSolverD\"))\n", - "await runtime.add_subscription(TypeSubscription(\"MathSolverA\", \"MathSolverB\"))\n", - "\n", - "# Subscriptions for topic published to by MathSolverB.\n", - "await runtime.add_subscription(TypeSubscription(\"MathSolverB\", \"MathSolverA\"))\n", - "await runtime.add_subscription(TypeSubscription(\"MathSolverB\", \"MathSolverC\"))\n", - "\n", - "# Subscriptions for topic published to by MathSolverC.\n", - "await runtime.add_subscription(TypeSubscription(\"MathSolverC\", \"MathSolverB\"))\n", - "await runtime.add_subscription(TypeSubscription(\"MathSolverC\", \"MathSolverD\"))\n", - "\n", - "# Subscriptions for topic published to by MathSolverD.\n", - "await runtime.add_subscription(TypeSubscription(\"MathSolverD\", \"MathSolverC\"))\n", - "await runtime.add_subscription(TypeSubscription(\"MathSolverD\", \"MathSolverA\"))\n", - "\n", - "# All solvers and the aggregator subscribe to the default topic." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Solving Math Problems\n", - "\n", - "Now let's run the debate to solve a math problem.\n", - "We publish a `SolverRequest` to the default topic, \n", - "and the aggregator agent will start the debate." - ] - }, - { - "cell_type": "code", - "execution_count": 44, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "--------------------------------------------------------------------------------\n", - "Aggregator MathAggregator:default received question:\n", - "Natalia sold clips to 48 of her friends in April, and then she sold half as many clips in May. How many clips did Natalia sell altogether in April and May?\n", - "--------------------------------------------------------------------------------\n", - "Aggregator MathAggregator:default publishes initial solver request.\n", - "--------------------------------------------------------------------------------\n", - "Solver MathSolverC:default round 0:\n", - "In April, Natalia sold 48 clips. In May, she sold half as many, which is 48 / 2 = 24 clips. To find the total number of clips sold in April and May, we add the amounts: 48 (April) + 24 (May) = 72 clips. \n", - "\n", - "Thus, the total number of clips sold by Natalia is {{72}}.\n", - "--------------------------------------------------------------------------------\n", - "Solver MathSolverB:default round 0:\n", - "In April, Natalia sold 48 clips. In May, she sold half as many clips, which is 48 / 2 = 24 clips. To find the total clips sold in April and May, we add both amounts: \n", - "\n", - "48 (April) + 24 (May) = 72.\n", - "\n", - "Thus, the total number of clips sold altogether is {{72}}.\n", - "--------------------------------------------------------------------------------\n", - "Solver MathSolverD:default round 0:\n", - "Natalia sold 48 clips in April. In May, she sold half as many, which is \\( \\frac{48}{2} = 24 \\) clips. To find the total clips sold in both months, we add the clips sold in April and May together:\n", - "\n", - "\\[ 48 + 24 = 72 \\]\n", - "\n", - "Thus, Natalia sold a total of 72 clips.\n", - "\n", - "The answer is {{72}}.\n", - "--------------------------------------------------------------------------------\n", - "Solver MathSolverC:default round 1:\n", - "Received all responses from 2 neighbors.\n", - "--------------------------------------------------------------------------------\n", - "Solver MathSolverA:default round 1:\n", - "Received all responses from 2 neighbors.\n", - "--------------------------------------------------------------------------------\n", - "Solver MathSolverA:default round 0:\n", - "In April, Natalia sold clips to 48 friends. In May, she sold half as many, which is calculated as follows:\n", - "\n", - "Half of 48 is \\( 48 \\div 2 = 24 \\).\n", - "\n", - "Now, to find the total clips sold in April and May, we add the totals from both months:\n", - "\n", - "\\( 48 + 24 = 72 \\).\n", - "\n", - "Thus, the total number of clips Natalia sold altogether in April and May is {{72}}.\n", - "--------------------------------------------------------------------------------\n", - "Solver MathSolverD:default round 1:\n", - "Received all responses from 2 neighbors.\n", - "--------------------------------------------------------------------------------\n", - "Solver MathSolverB:default round 1:\n", - "Received all responses from 2 neighbors.\n", - "--------------------------------------------------------------------------------\n", - "Solver MathSolverC:default round 1:\n", - "In April, Natalia sold 48 clips. In May, she sold half as many, which is 48 / 2 = 24 clips. The total number of clips sold in April and May is calculated by adding the two amounts: 48 (April) + 24 (May) = 72 clips. \n", - "\n", - "Therefore, the answer is {{72}}.\n", - "--------------------------------------------------------------------------------\n", - "Solver MathSolverA:default round 1:\n", - "In April, Natalia sold 48 clips. In May, she sold half of that amount, which is 48 / 2 = 24 clips. To find the total clips sold in both months, we sum the clips from April and May: \n", - "\n", - "48 (April) + 24 (May) = 72.\n", - "\n", - "Thus, Natalia sold a total of {{72}} clips. \n", - "\n", - "The answer is {{72}}.\n", - "--------------------------------------------------------------------------------\n", - "Solver MathSolverD:default round 2:\n", - "Received all responses from 2 neighbors.\n", - "--------------------------------------------------------------------------------\n", - "Solver MathSolverB:default round 2:\n", - "Received all responses from 2 neighbors.\n", - "--------------------------------------------------------------------------------\n", - "Solver MathSolverD:default round 1:\n", - "Natalia sold 48 clips in April. In May, she sold half of that, which is \\( 48 \\div 2 = 24 \\) clips. To find the total clips sold, we add the clips sold in both months:\n", - "\n", - "\\[ 48 + 24 = 72 \\]\n", - "\n", - "Therefore, the total number of clips sold by Natalia is {{72}}.\n", - "--------------------------------------------------------------------------------\n", - "Solver MathSolverB:default round 1:\n", - "In April, Natalia sold 48 clips. In May, she sold half that amount, which is 48 / 2 = 24 clips. To find the total clips sold in both months, we add the amounts: \n", - "\n", - "48 (April) + 24 (May) = 72.\n", - "\n", - "Therefore, the total number of clips sold altogether by Natalia is {{72}}.\n", - "--------------------------------------------------------------------------------\n", - "Solver MathSolverA:default round 2:\n", - "Received all responses from 2 neighbors.\n", - "--------------------------------------------------------------------------------\n", - "Solver MathSolverC:default round 2:\n", - "Received all responses from 2 neighbors.\n", - "--------------------------------------------------------------------------------\n", - "Solver MathSolverA:default round 2:\n", - "In April, Natalia sold 48 clips. In May, she sold half of that amount, which is \\( 48 \\div 2 = 24 \\) clips. To find the total clips sold in both months, we add the amounts from April and May:\n", - "\n", - "\\( 48 + 24 = 72 \\).\n", - "\n", - "Thus, the total number of clips sold by Natalia is {{72}}.\n", - "--------------------------------------------------------------------------------\n", - "Solver MathSolverC:default round 2:\n", - "In April, Natalia sold 48 clips. In May, she sold half of that amount, which is \\( 48 \\div 2 = 24 \\) clips. To find the total number of clips sold in both months, we add the clips sold in April and May: \n", - "\n", - "48 (April) + 24 (May) = 72. \n", - "\n", - "Thus, the total number of clips sold altogether by Natalia is {{72}}.\n", - "--------------------------------------------------------------------------------\n", - "Solver MathSolverB:default round 2:\n", - "In April, Natalia sold 48 clips. In May, she sold half as many, calculated as \\( 48 \\div 2 = 24 \\) clips. To find the total clips sold over both months, we sum the totals: \n", - "\n", - "\\( 48 (April) + 24 (May) = 72 \\).\n", - "\n", - "Therefore, the total number of clips Natalia sold is {{72}}.\n", - "--------------------------------------------------------------------------------\n", - "Solver MathSolverD:default round 2:\n", - "To solve the problem, we know that Natalia sold 48 clips in April. In May, she sold half that amount, which is calculated as \\( 48 \\div 2 = 24 \\) clips. To find the total number of clips sold over both months, we add the two amounts together:\n", - "\n", - "\\[ 48 + 24 = 72 \\]\n", - "\n", - "Thus, the total number of clips sold by Natalia is {{72}}.\n", - "--------------------------------------------------------------------------------\n", - "Aggregator MathAggregator:default received all final answers from 4 solvers.\n", - "--------------------------------------------------------------------------------\n", - "Aggregator MathAggregator:default publishes final answer:\n", - "72\n" - ] - } - ], - "source": [ - "question = \"Natalia sold clips to 48 of her friends in April, and then she sold half as many clips in May. How many clips did Natalia sell altogether in April and May?\"\n", - "runtime.start()\n", - "await runtime.publish_message(Question(content=question), DefaultTopicId())\n", - "await runtime.stop_when_idle()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } - }, - "nbformat": 4, - "nbformat_minor": 2 + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Multi-Agent Debate\n", + "\n", + "Multi-Agent Debate is a multi-agent design pattern that simulates a multi-turn interaction \n", + "where in each turn, agents exchange their responses with each other, and refine \n", + "their responses based on the responses from other agents.\n", + "\n", + "This example shows an implementation of the multi-agent debate pattern for solving\n", + "math problems from the [GSM8K benchmark](https://huggingface.co/datasets/openai/gsm8k).\n", + "\n", + "There are of two types of agents in this pattern: solver agents and an aggregator agent.\n", + "The solver agents are connected in a sparse manner following the technique described in\n", + "[Improving Multi-Agent Debate with Sparse Communication Topology](https://arxiv.org/abs/2406.11776).\n", + "The solver agents are responsible for solving math problems and exchanging responses with each other.\n", + "The aggregator agent is responsible for distributing math problems to the solver agents,\n", + "waiting for their final responses, and aggregating the responses to get the final answer.\n", + "\n", + "The pattern works as follows:\n", + "1. User sends a math problem to the aggregator agent.\n", + "2. The aggregator agent distributes the problem to the solver agents.\n", + "3. Each solver agent processes the problem, and publishes a response to its neighbors.\n", + "4. Each solver agent uses the responses from its neighbors to refine its response, and publishes a new response.\n", + "5. Repeat step 4 for a fixed number of rounds. In the final round, each solver agent publishes a final response.\n", + "6. The aggregator agent uses majority voting to aggregate the final responses from all solver agents to get a final answer, and publishes the answer.\n", + "\n", + "We will be using the broadcast API, i.e., {py:meth}`~autogen_core.base.BaseAgent.publish_message`,\n", + "and we will be using topic and subscription to implement the communication topology.\n", + "Read about [Topics and Subscriptions](../core-concepts/topic-and-subscription.md) to understand how they work." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [], + "source": [ + "import re\n", + "from dataclasses import dataclass\n", + "from typing import Dict, List\n", + "\n", + "from autogen_core import (\n", + " DefaultTopicId,\n", + " MessageContext,\n", + " RoutedAgent,\n", + " TypeSubscription,\n", + " default_subscription,\n", + " message_handler,\n", + ")\n", + "from autogen_core.application import SingleThreadedAgentRuntime\n", + "from autogen_core.components.models import (\n", + " AssistantMessage,\n", + " ChatCompletionClient,\n", + " LLMMessage,\n", + " SystemMessage,\n", + " UserMessage,\n", + ")\n", + "from autogen_ext.models import OpenAIChatCompletionClient" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Message Protocol\n", + "\n", + "First, we define the messages used by the agents.\n", + "`IntermediateSolverResponse` is the message exchanged among the solver agents in each round,\n", + "and `FinalSolverResponse` is the message published by the solver agents in the final round." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [], + "source": [ + "@dataclass\n", + "class Question:\n", + " content: str\n", + "\n", + "\n", + "@dataclass\n", + "class Answer:\n", + " content: str\n", + "\n", + "\n", + "@dataclass\n", + "class SolverRequest:\n", + " content: str\n", + " question: str\n", + "\n", + "\n", + "@dataclass\n", + "class IntermediateSolverResponse:\n", + " content: str\n", + " question: str\n", + " answer: str\n", + " round: int\n", + "\n", + "\n", + "@dataclass\n", + "class FinalSolverResponse:\n", + " answer: str" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Solver Agent\n", + "\n", + "The solver agent is responsible for solving math problems and exchanging responses with other solver agents.\n", + "Upon receiving a `SolverRequest`, the solver agent uses an LLM to generate an answer.\n", + "Then, it publishes a `IntermediateSolverResponse`\n", + "or a `FinalSolverResponse` based on the round number.\n", + "\n", + "The solver agent is given a topic type, which is used to indicate the topic\n", + "to which the agent should publish intermediate responses. This topic is subscribed\n", + "to by its neighbors to receive responses from this agent -- we will show\n", + "how this is done later.\n", + "\n", + "We use {py:meth}`~autogen_core.components.default_subscription` to let\n", + "solver agents subscribe to the default topic, which is used by the aggregator agent\n", + "to collect the final responses from the solver agents." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [], + "source": [ + "@default_subscription\n", + "class MathSolver(RoutedAgent):\n", + " def __init__(self, model_client: ChatCompletionClient, topic_type: str, num_neighbors: int, max_round: int) -> None:\n", + " super().__init__(\"A debator.\")\n", + " self._topic_type = topic_type\n", + " self._model_client = model_client\n", + " self._num_neighbors = num_neighbors\n", + " self._history: List[LLMMessage] = []\n", + " self._buffer: Dict[int, List[IntermediateSolverResponse]] = {}\n", + " self._system_messages = [\n", + " SystemMessage(\n", + " content=(\n", + " \"You are a helpful assistant with expertise in mathematics and reasoning. \"\n", + " \"Your task is to assist in solving a math reasoning problem by providing \"\n", + " \"a clear and detailed solution. Limit your output within 100 words, \"\n", + " \"and your final answer should be a single numerical number, \"\n", + " \"in the form of {{answer}}, at the end of your response. \"\n", + " \"For example, 'The answer is {{42}}.'\"\n", + " )\n", + " )\n", + " ]\n", + " self._round = 0\n", + " self._max_round = max_round\n", + "\n", + " @message_handler\n", + " async def handle_request(self, message: SolverRequest, ctx: MessageContext) -> None:\n", + " # Add the question to the memory.\n", + " self._history.append(UserMessage(content=message.content, source=\"user\"))\n", + " # Make an inference using the model.\n", + " model_result = await self._model_client.create(self._system_messages + self._history)\n", + " assert isinstance(model_result.content, str)\n", + " # Add the response to the memory.\n", + " self._history.append(AssistantMessage(content=model_result.content, source=self.metadata[\"type\"]))\n", + " print(f\"{'-'*80}\\nSolver {self.id} round {self._round}:\\n{model_result.content}\")\n", + " # Extract the answer from the response.\n", + " match = re.search(r\"\\{\\{(\\-?\\d+(\\.\\d+)?)\\}\\}\", model_result.content)\n", + " if match is None:\n", + " raise ValueError(\"The model response does not contain the answer.\")\n", + " answer = match.group(1)\n", + " # Increment the counter.\n", + " self._round += 1\n", + " if self._round == self._max_round:\n", + " # If the counter reaches the maximum round, publishes a final response.\n", + " await self.publish_message(FinalSolverResponse(answer=answer), topic_id=DefaultTopicId())\n", + " else:\n", + " # Publish intermediate response to the topic associated with this solver.\n", + " await self.publish_message(\n", + " IntermediateSolverResponse(\n", + " content=model_result.content,\n", + " question=message.question,\n", + " answer=answer,\n", + " round=self._round,\n", + " ),\n", + " topic_id=DefaultTopicId(type=self._topic_type),\n", + " )\n", + "\n", + " @message_handler\n", + " async def handle_response(self, message: IntermediateSolverResponse, ctx: MessageContext) -> None:\n", + " # Add neighbor's response to the buffer.\n", + " self._buffer.setdefault(message.round, []).append(message)\n", + " # Check if all neighbors have responded.\n", + " if len(self._buffer[message.round]) == self._num_neighbors:\n", + " print(\n", + " f\"{'-'*80}\\nSolver {self.id} round {message.round}:\\nReceived all responses from {self._num_neighbors} neighbors.\"\n", + " )\n", + " # Prepare the prompt for the next question.\n", + " prompt = \"These are the solutions to the problem from other agents:\\n\"\n", + " for resp in self._buffer[message.round]:\n", + " prompt += f\"One agent solution: {resp.content}\\n\"\n", + " prompt += (\n", + " \"Using the solutions from other agents as additional information, \"\n", + " \"can you provide your answer to the math problem? \"\n", + " f\"The original math problem is {message.question}. \"\n", + " \"Your final answer should be a single numerical number, \"\n", + " \"in the form of {{answer}}, at the end of your response.\"\n", + " )\n", + " # Send the question to the agent itself to solve.\n", + " await self.send_message(SolverRequest(content=prompt, question=message.question), self.id)\n", + " # Clear the buffer.\n", + " self._buffer.pop(message.round)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Aggregator Agent\n", + "\n", + "The aggregator agent is responsible for handling user question and \n", + "distributing math problems to the solver agents.\n", + "\n", + "The aggregator subscribes to the default topic using\n", + "{py:meth}`~autogen_core.components.default_subscription`. The default topic is used to\n", + "recieve user question, receive the final responses from the solver agents,\n", + "and publish the final answer back to the user.\n", + "\n", + "In a more complex application when you want to isolate the multi-agent debate into a\n", + "sub-component, you should use\n", + "{py:meth}`~autogen_core.components.type_subscription` to set a specific topic\n", + "type for the aggregator-solver communication, \n", + "and have the both the solver and aggregator publish and subscribe to that topic type." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [], + "source": [ + "@default_subscription\n", + "class MathAggregator(RoutedAgent):\n", + " def __init__(self, num_solvers: int) -> None:\n", + " super().__init__(\"Math Aggregator\")\n", + " self._num_solvers = num_solvers\n", + " self._buffer: List[FinalSolverResponse] = []\n", + "\n", + " @message_handler\n", + " async def handle_question(self, message: Question, ctx: MessageContext) -> None:\n", + " print(f\"{'-'*80}\\nAggregator {self.id} received question:\\n{message.content}\")\n", + " prompt = (\n", + " f\"Can you solve the following math problem?\\n{message.content}\\n\"\n", + " \"Explain your reasoning. Your final answer should be a single numerical number, \"\n", + " \"in the form of {{answer}}, at the end of your response.\"\n", + " )\n", + " print(f\"{'-'*80}\\nAggregator {self.id} publishes initial solver request.\")\n", + " await self.publish_message(SolverRequest(content=prompt, question=message.content), topic_id=DefaultTopicId())\n", + "\n", + " @message_handler\n", + " async def handle_final_solver_response(self, message: FinalSolverResponse, ctx: MessageContext) -> None:\n", + " self._buffer.append(message)\n", + " if len(self._buffer) == self._num_solvers:\n", + " print(f\"{'-'*80}\\nAggregator {self.id} received all final answers from {self._num_solvers} solvers.\")\n", + " # Find the majority answer.\n", + " answers = [resp.answer for resp in self._buffer]\n", + " majority_answer = max(set(answers), key=answers.count)\n", + " # Publish the aggregated response.\n", + " await self.publish_message(Answer(content=majority_answer), topic_id=DefaultTopicId())\n", + " # Clear the responses.\n", + " self._buffer.clear()\n", + " print(f\"{'-'*80}\\nAggregator {self.id} publishes final answer:\\n{majority_answer}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setting Up a Debate\n", + "\n", + "We will now set up a multi-agent debate with 4 solver agents and 1 aggregator agent.\n", + "The solver agents will be connected in a sparse manner as illustrated in the figure\n", + "below:\n", + "\n", + "```\n", + "A --- B\n", + "| |\n", + "| |\n", + "C --- D\n", + "```\n", + "\n", + "Each solver agent is connected to two other solver agents. \n", + "For example, agent A is connected to agents B and C.\n", + "\n", + "Let's first create a runtime and register the agent types." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AgentType(type='MathAggregator')" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "runtime = SingleThreadedAgentRuntime()\n", + "await MathSolver.register(\n", + " runtime,\n", + " \"MathSolverA\",\n", + " lambda: MathSolver(\n", + " model_client=OpenAIChatCompletionClient(model=\"gpt-4o-mini\"),\n", + " topic_type=\"MathSolverA\",\n", + " num_neighbors=2,\n", + " max_round=3,\n", + " ),\n", + ")\n", + "await MathSolver.register(\n", + " runtime,\n", + " \"MathSolverB\",\n", + " lambda: MathSolver(\n", + " model_client=OpenAIChatCompletionClient(model=\"gpt-4o-mini\"),\n", + " topic_type=\"MathSolverB\",\n", + " num_neighbors=2,\n", + " max_round=3,\n", + " ),\n", + ")\n", + "await MathSolver.register(\n", + " runtime,\n", + " \"MathSolverC\",\n", + " lambda: MathSolver(\n", + " model_client=OpenAIChatCompletionClient(model=\"gpt-4o-mini\"),\n", + " topic_type=\"MathSolverC\",\n", + " num_neighbors=2,\n", + " max_round=3,\n", + " ),\n", + ")\n", + "await MathSolver.register(\n", + " runtime,\n", + " \"MathSolverD\",\n", + " lambda: MathSolver(\n", + " model_client=OpenAIChatCompletionClient(model=\"gpt-4o-mini\"),\n", + " topic_type=\"MathSolverD\",\n", + " num_neighbors=2,\n", + " max_round=3,\n", + " ),\n", + ")\n", + "await MathAggregator.register(runtime, \"MathAggregator\", lambda: MathAggregator(num_solvers=4))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we will create the solver agent topology using {py:class}`~autogen_core.components.TypeSubscription`,\n", + "which maps each solver agent's publishing topic type to its neighbors' agent types." + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [], + "source": [ + "# Subscriptions for topic published to by MathSolverA.\n", + "await runtime.add_subscription(TypeSubscription(\"MathSolverA\", \"MathSolverD\"))\n", + "await runtime.add_subscription(TypeSubscription(\"MathSolverA\", \"MathSolverB\"))\n", + "\n", + "# Subscriptions for topic published to by MathSolverB.\n", + "await runtime.add_subscription(TypeSubscription(\"MathSolverB\", \"MathSolverA\"))\n", + "await runtime.add_subscription(TypeSubscription(\"MathSolverB\", \"MathSolverC\"))\n", + "\n", + "# Subscriptions for topic published to by MathSolverC.\n", + "await runtime.add_subscription(TypeSubscription(\"MathSolverC\", \"MathSolverB\"))\n", + "await runtime.add_subscription(TypeSubscription(\"MathSolverC\", \"MathSolverD\"))\n", + "\n", + "# Subscriptions for topic published to by MathSolverD.\n", + "await runtime.add_subscription(TypeSubscription(\"MathSolverD\", \"MathSolverC\"))\n", + "await runtime.add_subscription(TypeSubscription(\"MathSolverD\", \"MathSolverA\"))\n", + "\n", + "# All solvers and the aggregator subscribe to the default topic." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Solving Math Problems\n", + "\n", + "Now let's run the debate to solve a math problem.\n", + "We publish a `SolverRequest` to the default topic, \n", + "and the aggregator agent will start the debate." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--------------------------------------------------------------------------------\n", + "Aggregator MathAggregator:default received question:\n", + "Natalia sold clips to 48 of her friends in April, and then she sold half as many clips in May. How many clips did Natalia sell altogether in April and May?\n", + "--------------------------------------------------------------------------------\n", + "Aggregator MathAggregator:default publishes initial solver request.\n", + "--------------------------------------------------------------------------------\n", + "Solver MathSolverC:default round 0:\n", + "In April, Natalia sold 48 clips. In May, she sold half as many, which is 48 / 2 = 24 clips. To find the total number of clips sold in April and May, we add the amounts: 48 (April) + 24 (May) = 72 clips. \n", + "\n", + "Thus, the total number of clips sold by Natalia is {{72}}.\n", + "--------------------------------------------------------------------------------\n", + "Solver MathSolverB:default round 0:\n", + "In April, Natalia sold 48 clips. In May, she sold half as many clips, which is 48 / 2 = 24 clips. To find the total clips sold in April and May, we add both amounts: \n", + "\n", + "48 (April) + 24 (May) = 72.\n", + "\n", + "Thus, the total number of clips sold altogether is {{72}}.\n", + "--------------------------------------------------------------------------------\n", + "Solver MathSolverD:default round 0:\n", + "Natalia sold 48 clips in April. In May, she sold half as many, which is \\( \\frac{48}{2} = 24 \\) clips. To find the total clips sold in both months, we add the clips sold in April and May together:\n", + "\n", + "\\[ 48 + 24 = 72 \\]\n", + "\n", + "Thus, Natalia sold a total of 72 clips.\n", + "\n", + "The answer is {{72}}.\n", + "--------------------------------------------------------------------------------\n", + "Solver MathSolverC:default round 1:\n", + "Received all responses from 2 neighbors.\n", + "--------------------------------------------------------------------------------\n", + "Solver MathSolverA:default round 1:\n", + "Received all responses from 2 neighbors.\n", + "--------------------------------------------------------------------------------\n", + "Solver MathSolverA:default round 0:\n", + "In April, Natalia sold clips to 48 friends. In May, she sold half as many, which is calculated as follows:\n", + "\n", + "Half of 48 is \\( 48 \\div 2 = 24 \\).\n", + "\n", + "Now, to find the total clips sold in April and May, we add the totals from both months:\n", + "\n", + "\\( 48 + 24 = 72 \\).\n", + "\n", + "Thus, the total number of clips Natalia sold altogether in April and May is {{72}}.\n", + "--------------------------------------------------------------------------------\n", + "Solver MathSolverD:default round 1:\n", + "Received all responses from 2 neighbors.\n", + "--------------------------------------------------------------------------------\n", + "Solver MathSolverB:default round 1:\n", + "Received all responses from 2 neighbors.\n", + "--------------------------------------------------------------------------------\n", + "Solver MathSolverC:default round 1:\n", + "In April, Natalia sold 48 clips. In May, she sold half as many, which is 48 / 2 = 24 clips. The total number of clips sold in April and May is calculated by adding the two amounts: 48 (April) + 24 (May) = 72 clips. \n", + "\n", + "Therefore, the answer is {{72}}.\n", + "--------------------------------------------------------------------------------\n", + "Solver MathSolverA:default round 1:\n", + "In April, Natalia sold 48 clips. In May, she sold half of that amount, which is 48 / 2 = 24 clips. To find the total clips sold in both months, we sum the clips from April and May: \n", + "\n", + "48 (April) + 24 (May) = 72.\n", + "\n", + "Thus, Natalia sold a total of {{72}} clips. \n", + "\n", + "The answer is {{72}}.\n", + "--------------------------------------------------------------------------------\n", + "Solver MathSolverD:default round 2:\n", + "Received all responses from 2 neighbors.\n", + "--------------------------------------------------------------------------------\n", + "Solver MathSolverB:default round 2:\n", + "Received all responses from 2 neighbors.\n", + "--------------------------------------------------------------------------------\n", + "Solver MathSolverD:default round 1:\n", + "Natalia sold 48 clips in April. In May, she sold half of that, which is \\( 48 \\div 2 = 24 \\) clips. To find the total clips sold, we add the clips sold in both months:\n", + "\n", + "\\[ 48 + 24 = 72 \\]\n", + "\n", + "Therefore, the total number of clips sold by Natalia is {{72}}.\n", + "--------------------------------------------------------------------------------\n", + "Solver MathSolverB:default round 1:\n", + "In April, Natalia sold 48 clips. In May, she sold half that amount, which is 48 / 2 = 24 clips. To find the total clips sold in both months, we add the amounts: \n", + "\n", + "48 (April) + 24 (May) = 72.\n", + "\n", + "Therefore, the total number of clips sold altogether by Natalia is {{72}}.\n", + "--------------------------------------------------------------------------------\n", + "Solver MathSolverA:default round 2:\n", + "Received all responses from 2 neighbors.\n", + "--------------------------------------------------------------------------------\n", + "Solver MathSolverC:default round 2:\n", + "Received all responses from 2 neighbors.\n", + "--------------------------------------------------------------------------------\n", + "Solver MathSolverA:default round 2:\n", + "In April, Natalia sold 48 clips. In May, she sold half of that amount, which is \\( 48 \\div 2 = 24 \\) clips. To find the total clips sold in both months, we add the amounts from April and May:\n", + "\n", + "\\( 48 + 24 = 72 \\).\n", + "\n", + "Thus, the total number of clips sold by Natalia is {{72}}.\n", + "--------------------------------------------------------------------------------\n", + "Solver MathSolverC:default round 2:\n", + "In April, Natalia sold 48 clips. In May, she sold half of that amount, which is \\( 48 \\div 2 = 24 \\) clips. To find the total number of clips sold in both months, we add the clips sold in April and May: \n", + "\n", + "48 (April) + 24 (May) = 72. \n", + "\n", + "Thus, the total number of clips sold altogether by Natalia is {{72}}.\n", + "--------------------------------------------------------------------------------\n", + "Solver MathSolverB:default round 2:\n", + "In April, Natalia sold 48 clips. In May, she sold half as many, calculated as \\( 48 \\div 2 = 24 \\) clips. To find the total clips sold over both months, we sum the totals: \n", + "\n", + "\\( 48 (April) + 24 (May) = 72 \\).\n", + "\n", + "Therefore, the total number of clips Natalia sold is {{72}}.\n", + "--------------------------------------------------------------------------------\n", + "Solver MathSolverD:default round 2:\n", + "To solve the problem, we know that Natalia sold 48 clips in April. In May, she sold half that amount, which is calculated as \\( 48 \\div 2 = 24 \\) clips. To find the total number of clips sold over both months, we add the two amounts together:\n", + "\n", + "\\[ 48 + 24 = 72 \\]\n", + "\n", + "Thus, the total number of clips sold by Natalia is {{72}}.\n", + "--------------------------------------------------------------------------------\n", + "Aggregator MathAggregator:default received all final answers from 4 solvers.\n", + "--------------------------------------------------------------------------------\n", + "Aggregator MathAggregator:default publishes final answer:\n", + "72\n" + ] + } + ], + "source": [ + "question = \"Natalia sold clips to 48 of her friends in April, and then she sold half as many clips in May. How many clips did Natalia sell altogether in April and May?\"\n", + "runtime.start()\n", + "await runtime.publish_message(Question(content=question), DefaultTopicId())\n", + "await runtime.stop_when_idle()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/model-clients.ipynb b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/model-clients.ipynb index 1dbb0840bb94..f430f1fff568 100644 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/model-clients.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/model-clients.ipynb @@ -1,621 +1,621 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Model Clients\n", - "\n", - "AutoGen provides the {py:mod}`autogen_core.components.models` module with a suite of built-in\n", - "model clients for using ChatCompletion API.\n", - "All model clients implement the {py:class}`~autogen_core.components.models.ChatCompletionClient` protocol class." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Built-in Model Clients\n", - "\n", - "Currently there are two built-in model clients:\n", - "{py:class}`~autogen_ext.models.OpenAIChatCompletionClient` and\n", - "{py:class}`~autogen_ext.models.AzureOpenAIChatCompletionClient`.\n", - "Both clients are asynchronous.\n", - "\n", - "To use the {py:class}`~autogen_ext.models.OpenAIChatCompletionClient`, you need to provide the API key\n", - "either through the environment variable `OPENAI_API_KEY` or through the `api_key` argument." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from autogen_core.components.models import UserMessage\n", - "from autogen_ext.models import OpenAIChatCompletionClient\n", - "\n", - "# Create an OpenAI model client.\n", - "model_client = OpenAIChatCompletionClient(\n", - " model=\"gpt-4o\",\n", - " # api_key=\"sk-...\", # Optional if you have an API key set in the environment.\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can call the {py:meth}`~autogen_ext.models.OpenAIChatCompletionClient.create` method to create a\n", - "chat completion request, and await for an {py:class}`~autogen_core.components.models.CreateResult` object in return." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The capital of France is Paris.\n" - ] - } - ], - "source": [ - "# Send a message list to the model and await the response.\n", - "messages = [\n", - " UserMessage(content=\"What is the capital of France?\", source=\"user\"),\n", - "]\n", - "response = await model_client.create(messages=messages)\n", - "\n", - "# Print the response\n", - "print(response.content)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "RequestUsage(prompt_tokens=15, completion_tokens=7)\n" - ] - } - ], - "source": [ - "# Print the response token usage\n", - "print(response.usage)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Streaming Response\n", - "\n", - "You can use the {py:meth}`~autogen_ext.models.OpenAIChatCompletionClient.create_streaming` method to create a\n", - "chat completion request with streaming response." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Streamed responses:\n", - "In the heart of an ancient forest, beneath the shadow of snow-capped peaks, a dragon named Elara lived secretly for centuries. Elara was unlike any dragon from the old tales; her scales shimmered with a deep emerald hue, each scale engraved with symbols of lost wisdom. The villagers in the nearby valley spoke of mysterious lights dancing across the night sky, but none dared venture close enough to solve the enigma.\n", - "\n", - "One cold winter's eve, a young girl named Lira, brimming with curiosity and armed with the innocence of youth, wandered into Elara’s domain. Instead of fire and fury, she found warmth and a gentle gaze. The dragon shared stories of a world long forgotten and in return, Lira gifted her simple stories of human life, rich in laughter and scent of earth.\n", - "\n", - "From that night on, the villagers noticed subtle changes—the crops grew taller, and the air seemed sweeter. Elara had infused the valley with ancient magic, a guardian of balance, watching quietly as her new friend thrived under the stars. And so, Lira and Elara’s bond marked the beginning of a timeless friendship that spun tales of hope whispered through the leaves of the ever-verdant forest.\n", - "\n", - "------------\n", - "\n", - "The complete response:\n", - "In the heart of an ancient forest, beneath the shadow of snow-capped peaks, a dragon named Elara lived secretly for centuries. Elara was unlike any dragon from the old tales; her scales shimmered with a deep emerald hue, each scale engraved with symbols of lost wisdom. The villagers in the nearby valley spoke of mysterious lights dancing across the night sky, but none dared venture close enough to solve the enigma.\n", - "\n", - "One cold winter's eve, a young girl named Lira, brimming with curiosity and armed with the innocence of youth, wandered into Elara’s domain. Instead of fire and fury, she found warmth and a gentle gaze. The dragon shared stories of a world long forgotten and in return, Lira gifted her simple stories of human life, rich in laughter and scent of earth.\n", - "\n", - "From that night on, the villagers noticed subtle changes—the crops grew taller, and the air seemed sweeter. Elara had infused the valley with ancient magic, a guardian of balance, watching quietly as her new friend thrived under the stars. And so, Lira and Elara’s bond marked the beginning of a timeless friendship that spun tales of hope whispered through the leaves of the ever-verdant forest.\n", - "\n", - "\n", - "------------\n", - "\n", - "The token usage was:\n", - "RequestUsage(prompt_tokens=0, completion_tokens=0)\n" - ] - } - ], - "source": [ - "messages = [\n", - " UserMessage(content=\"Write a very short story about a dragon.\", source=\"user\"),\n", - "]\n", - "\n", - "# Create a stream.\n", - "stream = model_client.create_stream(messages=messages)\n", - "\n", - "# Iterate over the stream and print the responses.\n", - "print(\"Streamed responses:\")\n", - "async for response in stream: # type: ignore\n", - " if isinstance(response, str):\n", - " # A partial response is a string.\n", - " print(response, flush=True, end=\"\")\n", - " else:\n", - " # The last response is a CreateResult object with the complete message.\n", - " print(\"\\n\\n------------\\n\")\n", - " print(\"The complete response:\", flush=True)\n", - " print(response.content, flush=True)\n", - " print(\"\\n\\n------------\\n\")\n", - " print(\"The token usage was:\", flush=True)\n", - " print(response.usage, flush=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "```{note}\n", - "The last response in the streaming response is always the final response\n", - "of the type {py:class}`~autogen_core.components.models.CreateResult`.\n", - "```\n", - "\n", - "**NB the default usage response is to return zero values**" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### A Note on Token usage counts with streaming example\n", - "Comparing usage returns in the above Non Streaming `model_client.create(messages=messages)` vs streaming `model_client.create_stream(messages=messages)` we see differences.\n", - "The non streaming response by default returns valid prompt and completion token usage counts. \n", - "The streamed response by default returns zero values.\n", - "\n", - "as documented in the OPENAI API Reference an additional parameter `stream_options` can be specified to return valid usage counts. see [stream_options](https://platform.openai.com/docs/api-reference/chat/create#chat-create-stream_options)\n", - "\n", - "Only set this when you using streaming ie , using `create_stream` \n", - "\n", - "to enable this in `create_stream` set `extra_create_args={\"stream_options\": {\"include_usage\": True}},`\n", - "\n", - "- **Note whilst other API's like LiteLLM also support this, it is not always guarenteed that it is fully supported or correct**\n", - "\n", - "#### Streaming example with token usage\n" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Streamed responses:\n", - "In a lush, emerald valley hidden by towering peaks, there lived a dragon named Ember. Unlike others of her kind, Ember cherished solitude over treasure, and the songs of the stream over the roar of flames. One misty dawn, a young shepherd stumbled into her sanctuary, lost and frightened. \n", - "\n", - "Instead of fury, he was met with kindness as Ember extended a wing, guiding him back to safety. In gratitude, the shepherd visited yearly, bringing tales of his world beyond the mountains. Over time, a friendship blossomed, binding man and dragon in shared stories and laughter.\n", - "\n", - "As the years passed, the legend of Ember the gentle-hearted spread far and wide, forever changing the way dragons were seen in the hearts of many.\n", - "\n", - "------------\n", - "\n", - "The complete response:\n", - "In a lush, emerald valley hidden by towering peaks, there lived a dragon named Ember. Unlike others of her kind, Ember cherished solitude over treasure, and the songs of the stream over the roar of flames. One misty dawn, a young shepherd stumbled into her sanctuary, lost and frightened. \n", - "\n", - "Instead of fury, he was met with kindness as Ember extended a wing, guiding him back to safety. In gratitude, the shepherd visited yearly, bringing tales of his world beyond the mountains. Over time, a friendship blossomed, binding man and dragon in shared stories and laughter.\n", - "\n", - "As the years passed, the legend of Ember the gentle-hearted spread far and wide, forever changing the way dragons were seen in the hearts of many.\n", - "\n", - "\n", - "------------\n", - "\n", - "The token usage was:\n", - "RequestUsage(prompt_tokens=17, completion_tokens=146)\n" - ] - } - ], - "source": [ - "messages = [\n", - " UserMessage(content=\"Write a very short story about a dragon.\", source=\"user\"),\n", - "]\n", - "\n", - "# Create a stream.\n", - "stream = model_client.create_stream(messages=messages, extra_create_args={\"stream_options\": {\"include_usage\": True}})\n", - "\n", - "# Iterate over the stream and print the responses.\n", - "print(\"Streamed responses:\")\n", - "async for response in stream: # type: ignore\n", - " if isinstance(response, str):\n", - " # A partial response is a string.\n", - " print(response, flush=True, end=\"\")\n", - " else:\n", - " # The last response is a CreateResult object with the complete message.\n", - " print(\"\\n\\n------------\\n\")\n", - " print(\"The complete response:\", flush=True)\n", - " print(response.content, flush=True)\n", - " print(\"\\n\\n------------\\n\")\n", - " print(\"The token usage was:\", flush=True)\n", - " print(response.usage, flush=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Azure OpenAI\n", - "\n", - "To use the {py:class}`~autogen_ext.models.AzureOpenAIChatCompletionClient`, you need to provide\n", - "the deployment id, Azure Cognitive Services endpoint, api version, and model capabilities.\n", - "For authentication, you can either provide an API key or an Azure Active Directory (AAD) token credential.\n", - "To use AAD authentication, you need to first install the `azure-identity` package." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "vscode": { - "languageId": "shellscript" - } - }, - "outputs": [], - "source": [ - "# pip install azure-identity" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The following code snippet shows how to use AAD authentication.\n", - "The identity used must be assigned the [**Cognitive Services OpenAI User**](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/role-based-access-control#cognitive-services-openai-user) role." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from autogen_ext.models import AzureOpenAIChatCompletionClient\n", - "from azure.identity import DefaultAzureCredential, get_bearer_token_provider\n", - "\n", - "# Create the token provider\n", - "token_provider = get_bearer_token_provider(DefaultAzureCredential(), \"https://cognitiveservices.azure.com/.default\")\n", - "\n", - "az_model_client = AzureOpenAIChatCompletionClient(\n", - " azure_deployment=\"{your-azure-deployment}\",\n", - " model=\"{model-name, such as gpt-4o}\",\n", - " api_version=\"2024-06-01\",\n", - " azure_endpoint=\"https://{your-custom-endpoint}.openai.azure.com/\",\n", - " azure_ad_token_provider=token_provider, # Optional if you choose key-based authentication.\n", - " # api_key=\"sk-...\", # For key-based authentication.\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "```{note}\n", - "See [here](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/managed-identity#chat-completions) for how to use the Azure client directly or for more info.\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Build Agent using Model Client\n", - "\n", - "Let's create a simple AI agent that can respond to messages using the ChatCompletion API." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "from dataclasses import dataclass\n", - "\n", - "from autogen_core import MessageContext, RoutedAgent, message_handler\n", - "from autogen_core.application import SingleThreadedAgentRuntime\n", - "from autogen_core.components.models import ChatCompletionClient, SystemMessage, UserMessage\n", - "from autogen_ext.models import OpenAIChatCompletionClient\n", - "\n", - "\n", - "@dataclass\n", - "class Message:\n", - " content: str\n", - "\n", - "\n", - "class SimpleAgent(RoutedAgent):\n", - " def __init__(self, model_client: ChatCompletionClient) -> None:\n", - " super().__init__(\"A simple agent\")\n", - " self._system_messages = [SystemMessage(\"You are a helpful AI assistant.\")]\n", - " self._model_client = model_client\n", - "\n", - " @message_handler\n", - " async def handle_user_message(self, message: Message, ctx: MessageContext) -> Message:\n", - " # Prepare input to the chat completion model.\n", - " user_message = UserMessage(content=message.content, source=\"user\")\n", - " response = await self._model_client.create(\n", - " self._system_messages + [user_message], cancellation_token=ctx.cancellation_token\n", - " )\n", - " # Return with the model's response.\n", - " assert isinstance(response.content, str)\n", - " return Message(content=response.content)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The `SimpleAgent` class is a subclass of the\n", - "{py:class}`autogen_core.components.RoutedAgent` class for the convenience of automatically routing messages to the appropriate handlers.\n", - "It has a single handler, `handle_user_message`, which handles message from the user. It uses the `ChatCompletionClient` to generate a response to the message.\n", - "It then returns the response to the user, following the direct communication model.\n", - "\n", - "```{note}\n", - "The `cancellation_token` of the type {py:class}`autogen_core.base.CancellationToken` is used to cancel\n", - "asynchronous operations. It is linked to async calls inside the message handlers\n", - "and can be used by the caller to cancel the handlers.\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Seattle is a vibrant city with a wide range of activities and attractions. Here are some fun things to do in Seattle:\n", - "\n", - "1. **Space Needle**: Visit this iconic observation tower for stunning views of the city and surrounding mountains.\n", - "\n", - "2. **Pike Place Market**: Explore this historic market where you can see the famous fish toss, buy local produce, and find unique crafts and eateries.\n", - "\n", - "3. **Museum of Pop Culture (MoPOP)**: Dive into the world of contemporary culture, music, and science fiction at this interactive museum.\n", - "\n", - "4. **Chihuly Garden and Glass**: Marvel at the beautiful glass art installations by artist Dale Chihuly, located right next to the Space Needle.\n", - "\n", - "5. **Seattle Aquarium**: Discover the diverse marine life of the Pacific Northwest at this engaging aquarium.\n", - "\n", - "6. **Seattle Art Museum**: Explore a vast collection of art from around the world, including contemporary and indigenous art.\n", - "\n", - "7. **Kerry Park**: For one of the best views of the Seattle skyline, head to this small park on Queen Anne Hill.\n", - "\n", - "8. **Ballard Locks**: Watch boats pass through the locks and observe the salmon ladder to see salmon migrating.\n", - "\n", - "9. **Ferry to Bainbridge Island**: Take a scenic ferry ride across Puget Sound to enjoy charming shops, restaurants, and beautiful natural scenery.\n", - "\n", - "10. **Olympic Sculpture Park**: Stroll through this outdoor park with large-scale sculptures and stunning views of the waterfront and mountains.\n", - "\n", - "11. **Underground Tour**: Discover Seattle's history on this quirky tour of the city's underground passageways in Pioneer Square.\n", - "\n", - "12. **Seattle Waterfront**: Enjoy the shops, restaurants, and attractions along the waterfront, including the Seattle Great Wheel and the aquarium.\n", - "\n", - "13. **Discovery Park**: Explore the largest green space in Seattle, featuring trails, beaches, and views of Puget Sound.\n", - "\n", - "14. **Food Tours**: Try out Seattle’s diverse culinary scene, including fresh seafood, international cuisines, and coffee culture (don’t miss the original Starbucks!).\n", - "\n", - "15. **Attend a Sports Game**: Catch a Seahawks (NFL), Mariners (MLB), or Sounders (MLS) game for a lively local experience.\n", - "\n", - "Whether you're interested in culture, nature, food, or history, Seattle has something for everyone to enjoy!\n" - ] - } - ], - "source": [ - "# Create the runtime and register the agent.\n", - "from autogen_core import AgentId\n", - "\n", - "runtime = SingleThreadedAgentRuntime()\n", - "await SimpleAgent.register(\n", - " runtime,\n", - " \"simple_agent\",\n", - " lambda: SimpleAgent(\n", - " OpenAIChatCompletionClient(\n", - " model=\"gpt-4o-mini\",\n", - " # api_key=\"sk-...\", # Optional if you have an OPENAI_API_KEY set in the environment.\n", - " )\n", - " ),\n", - ")\n", - "# Start the runtime processing messages.\n", - "runtime.start()\n", - "# Send a message to the agent and get the response.\n", - "message = Message(\"Hello, what are some fun things to do in Seattle?\")\n", - "response = await runtime.send_message(message, AgentId(\"simple_agent\", \"default\"))\n", - "print(response.content)\n", - "# Stop the runtime processing messages.\n", - "await runtime.stop()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Manage Model Context\n", - "\n", - "The above `SimpleAgent` always responds with a fresh context that contains only\n", - "the system message and the latest user's message.\n", - "We can use model context classes from {py:mod}`autogen_core.components.model_context`\n", - "to make the agent \"remember\" previous conversations.\n", - "A model context supports storage and retrieval of Chat Completion messages.\n", - "It is always used together with a model client to generate LLM-based responses.\n", - "\n", - "For example, {py:mod}`~autogen_core.components.model_context.BufferedChatCompletionContext`\n", - "is a most-recent-used (MRU) context that stores the most recent `buffer_size`\n", - "number of messages. This is useful to avoid context overflow in many LLMs.\n", - "\n", - "Let's update the previous example to use\n", - "{py:mod}`~autogen_core.components.model_context.BufferedChatCompletionContext`." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "from autogen_core.components.model_context import BufferedChatCompletionContext\n", - "from autogen_core.components.models import AssistantMessage\n", - "\n", - "\n", - "class SimpleAgentWithContext(RoutedAgent):\n", - " def __init__(self, model_client: ChatCompletionClient) -> None:\n", - " super().__init__(\"A simple agent\")\n", - " self._system_messages = [SystemMessage(\"You are a helpful AI assistant.\")]\n", - " self._model_client = model_client\n", - " self._model_context = BufferedChatCompletionContext(buffer_size=5)\n", - "\n", - " @message_handler\n", - " async def handle_user_message(self, message: Message, ctx: MessageContext) -> Message:\n", - " # Prepare input to the chat completion model.\n", - " user_message = UserMessage(content=message.content, source=\"user\")\n", - " # Add message to model context.\n", - " await self._model_context.add_message(user_message)\n", - " # Generate a response.\n", - " response = await self._model_client.create(\n", - " self._system_messages + (await self._model_context.get_messages()),\n", - " cancellation_token=ctx.cancellation_token,\n", - " )\n", - " # Return with the model's response.\n", - " assert isinstance(response.content, str)\n", - " # Add message to model context.\n", - " await self._model_context.add_message(AssistantMessage(content=response.content, source=self.metadata[\"type\"]))\n", - " return Message(content=response.content)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now let's try to ask follow up questions after the first one." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Question: Hello, what are some fun things to do in Seattle?\n", - "Response: Seattle offers a wide variety of fun activities and attractions for visitors. Here are some highlights:\n", - "\n", - "1. **Pike Place Market**: Explore this iconic market, where you can find fresh produce, unique crafts, and the famous fish-throwing vendors. Don’t forget to visit the original Starbucks!\n", - "\n", - "2. **Space Needle**: Enjoy breathtaking views of the city and Mount Rainier from the observation deck of this iconic structure. You can also dine at the SkyCity restaurant.\n", - "\n", - "3. **Chihuly Garden and Glass**: Admire the stunning glass art installations created by artist Dale Chihuly. The garden and exhibit are particularly beautiful, especially in good weather.\n", - "\n", - "4. **Museum of Pop Culture (MoPOP)**: Dive into the world of music, science fiction, and pop culture through interactive exhibits and memorabilia.\n", - "\n", - "5. **Seattle Aquarium**: Located on the waterfront, the aquarium features a variety of marine life native to the Pacific Northwest, including otters and diving birds.\n", - "\n", - "6. **Seattle Art Museum (SAM)**: Explore a diverse collection of art from around the world, including Native American art and contemporary pieces.\n", - "\n", - "7. **Ballard Locks**: Watch boats travel between the Puget Sound and Lake Union, and see salmon navigating the fish ladder during spawning season.\n", - "\n", - "8. **Fremont Troll**: Visit this quirky public art installation located under the Aurora Bridge, where you can take fun photos with the giant troll.\n", - "\n", - "9. **Kerry Park**: For a picturesque view of the Seattle skyline, head to Kerry Park on Queen Anne Hill, especially at sunset.\n", - "\n", - "10. **Take a Ferry Ride**: Enjoy the scenic views while taking a ferry to nearby Bainbridge Island or Vashon Island for a relaxing day trip.\n", - "\n", - "11. **Underground Tour**: Explore Seattle’s history on an entertaining underground tour in Pioneer Square, where you’ll learn about the city’s early days.\n", - "\n", - "12. **Attend a Sporting Event**: Depending on the season, catch a Seattle Seahawks (NFL) game, a Seattle Mariners (MLB) game, or a Seattle Sounders (MLS) match.\n", - "\n", - "13. **Explore Discovery Park**: Enjoy nature with hiking trails, beach access, and stunning views of the Puget Sound and Olympic Mountains.\n", - "\n", - "14. **West Seattle’s Alki Beach**: Relax at this beach with beautiful views of the Seattle skyline and enjoy beachside activities like biking or kayaking.\n", - "\n", - "15. **Dining and Craft Beer**: Seattle has a vibrant food scene and is known for its seafood, coffee culture, and craft breweries. Make sure to explore local restaurants and breweries.\n", - "\n", - "There’s something for everyone in Seattle, whether you’re interested in nature, art, history, or food!\n", - "-----\n", - "Question: What was the first thing you mentioned?\n", - "Response: The first thing I mentioned was **Pike Place Market**, an iconic market in Seattle where you can find fresh produce, unique crafts, and experience the famous fish-throwing vendors. It's also home to the original Starbucks and various charming shops and eateries.\n" - ] - } - ], - "source": [ - "runtime = SingleThreadedAgentRuntime()\n", - "await SimpleAgentWithContext.register(\n", - " runtime,\n", - " \"simple_agent_context\",\n", - " lambda: SimpleAgentWithContext(\n", - " OpenAIChatCompletionClient(\n", - " model=\"gpt-4o-mini\",\n", - " # api_key=\"sk-...\", # Optional if you have an OPENAI_API_KEY set in the environment.\n", - " )\n", - " ),\n", - ")\n", - "# Start the runtime processing messages.\n", - "runtime.start()\n", - "agent_id = AgentId(\"simple_agent_context\", \"default\")\n", - "\n", - "# First question.\n", - "message = Message(\"Hello, what are some fun things to do in Seattle?\")\n", - "print(f\"Question: {message.content}\")\n", - "response = await runtime.send_message(message, agent_id)\n", - "print(f\"Response: {response.content}\")\n", - "print(\"-----\")\n", - "\n", - "# Second question.\n", - "message = Message(\"What was the first thing you mentioned?\")\n", - "print(f\"Question: {message.content}\")\n", - "response = await runtime.send_message(message, agent_id)\n", - "print(f\"Response: {response.content}\")\n", - "\n", - "# Stop the runtime processing messages.\n", - "await runtime.stop()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "From the second response, you can see the agent now can recall its own previous responses." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - } - }, - "nbformat": 4, - "nbformat_minor": 2 + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Model Clients\n", + "\n", + "AutoGen provides the {py:mod}`autogen_core.components.models` module with a suite of built-in\n", + "model clients for using ChatCompletion API.\n", + "All model clients implement the {py:class}`~autogen_core.components.models.ChatCompletionClient` protocol class." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Built-in Model Clients\n", + "\n", + "Currently there are two built-in model clients:\n", + "{py:class}`~autogen_ext.models.OpenAIChatCompletionClient` and\n", + "{py:class}`~autogen_ext.models.AzureOpenAIChatCompletionClient`.\n", + "Both clients are asynchronous.\n", + "\n", + "To use the {py:class}`~autogen_ext.models.OpenAIChatCompletionClient`, you need to provide the API key\n", + "either through the environment variable `OPENAI_API_KEY` or through the `api_key` argument." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from autogen_core.components.models import UserMessage\n", + "from autogen_ext.models import OpenAIChatCompletionClient\n", + "\n", + "# Create an OpenAI model client.\n", + "model_client = OpenAIChatCompletionClient(\n", + " model=\"gpt-4o\",\n", + " # api_key=\"sk-...\", # Optional if you have an API key set in the environment.\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can call the {py:meth}`~autogen_ext.models.OpenAIChatCompletionClient.create` method to create a\n", + "chat completion request, and await for an {py:class}`~autogen_core.components.models.CreateResult` object in return." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The capital of France is Paris.\n" + ] + } + ], + "source": [ + "# Send a message list to the model and await the response.\n", + "messages = [\n", + " UserMessage(content=\"What is the capital of France?\", source=\"user\"),\n", + "]\n", + "response = await model_client.create(messages=messages)\n", + "\n", + "# Print the response\n", + "print(response.content)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "RequestUsage(prompt_tokens=15, completion_tokens=7)\n" + ] + } + ], + "source": [ + "# Print the response token usage\n", + "print(response.usage)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Streaming Response\n", + "\n", + "You can use the {py:meth}`~autogen_ext.models.OpenAIChatCompletionClient.create_streaming` method to create a\n", + "chat completion request with streaming response." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Streamed responses:\n", + "In the heart of an ancient forest, beneath the shadow of snow-capped peaks, a dragon named Elara lived secretly for centuries. Elara was unlike any dragon from the old tales; her scales shimmered with a deep emerald hue, each scale engraved with symbols of lost wisdom. The villagers in the nearby valley spoke of mysterious lights dancing across the night sky, but none dared venture close enough to solve the enigma.\n", + "\n", + "One cold winter's eve, a young girl named Lira, brimming with curiosity and armed with the innocence of youth, wandered into Elara’s domain. Instead of fire and fury, she found warmth and a gentle gaze. The dragon shared stories of a world long forgotten and in return, Lira gifted her simple stories of human life, rich in laughter and scent of earth.\n", + "\n", + "From that night on, the villagers noticed subtle changes—the crops grew taller, and the air seemed sweeter. Elara had infused the valley with ancient magic, a guardian of balance, watching quietly as her new friend thrived under the stars. And so, Lira and Elara’s bond marked the beginning of a timeless friendship that spun tales of hope whispered through the leaves of the ever-verdant forest.\n", + "\n", + "------------\n", + "\n", + "The complete response:\n", + "In the heart of an ancient forest, beneath the shadow of snow-capped peaks, a dragon named Elara lived secretly for centuries. Elara was unlike any dragon from the old tales; her scales shimmered with a deep emerald hue, each scale engraved with symbols of lost wisdom. The villagers in the nearby valley spoke of mysterious lights dancing across the night sky, but none dared venture close enough to solve the enigma.\n", + "\n", + "One cold winter's eve, a young girl named Lira, brimming with curiosity and armed with the innocence of youth, wandered into Elara’s domain. Instead of fire and fury, she found warmth and a gentle gaze. The dragon shared stories of a world long forgotten and in return, Lira gifted her simple stories of human life, rich in laughter and scent of earth.\n", + "\n", + "From that night on, the villagers noticed subtle changes—the crops grew taller, and the air seemed sweeter. Elara had infused the valley with ancient magic, a guardian of balance, watching quietly as her new friend thrived under the stars. And so, Lira and Elara’s bond marked the beginning of a timeless friendship that spun tales of hope whispered through the leaves of the ever-verdant forest.\n", + "\n", + "\n", + "------------\n", + "\n", + "The token usage was:\n", + "RequestUsage(prompt_tokens=0, completion_tokens=0)\n" + ] + } + ], + "source": [ + "messages = [\n", + " UserMessage(content=\"Write a very short story about a dragon.\", source=\"user\"),\n", + "]\n", + "\n", + "# Create a stream.\n", + "stream = model_client.create_stream(messages=messages)\n", + "\n", + "# Iterate over the stream and print the responses.\n", + "print(\"Streamed responses:\")\n", + "async for response in stream: # type: ignore\n", + " if isinstance(response, str):\n", + " # A partial response is a string.\n", + " print(response, flush=True, end=\"\")\n", + " else:\n", + " # The last response is a CreateResult object with the complete message.\n", + " print(\"\\n\\n------------\\n\")\n", + " print(\"The complete response:\", flush=True)\n", + " print(response.content, flush=True)\n", + " print(\"\\n\\n------------\\n\")\n", + " print(\"The token usage was:\", flush=True)\n", + " print(response.usage, flush=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```{note}\n", + "The last response in the streaming response is always the final response\n", + "of the type {py:class}`~autogen_core.components.models.CreateResult`.\n", + "```\n", + "\n", + "**NB the default usage response is to return zero values**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### A Note on Token usage counts with streaming example\n", + "Comparing usage returns in the above Non Streaming `model_client.create(messages=messages)` vs streaming `model_client.create_stream(messages=messages)` we see differences.\n", + "The non streaming response by default returns valid prompt and completion token usage counts. \n", + "The streamed response by default returns zero values.\n", + "\n", + "as documented in the OPENAI API Reference an additional parameter `stream_options` can be specified to return valid usage counts. see [stream_options](https://platform.openai.com/docs/api-reference/chat/create#chat-create-stream_options)\n", + "\n", + "Only set this when you using streaming ie , using `create_stream` \n", + "\n", + "to enable this in `create_stream` set `extra_create_args={\"stream_options\": {\"include_usage\": True}},`\n", + "\n", + "- **Note whilst other API's like LiteLLM also support this, it is not always guarenteed that it is fully supported or correct**\n", + "\n", + "#### Streaming example with token usage\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Streamed responses:\n", + "In a lush, emerald valley hidden by towering peaks, there lived a dragon named Ember. Unlike others of her kind, Ember cherished solitude over treasure, and the songs of the stream over the roar of flames. One misty dawn, a young shepherd stumbled into her sanctuary, lost and frightened. \n", + "\n", + "Instead of fury, he was met with kindness as Ember extended a wing, guiding him back to safety. In gratitude, the shepherd visited yearly, bringing tales of his world beyond the mountains. Over time, a friendship blossomed, binding man and dragon in shared stories and laughter.\n", + "\n", + "As the years passed, the legend of Ember the gentle-hearted spread far and wide, forever changing the way dragons were seen in the hearts of many.\n", + "\n", + "------------\n", + "\n", + "The complete response:\n", + "In a lush, emerald valley hidden by towering peaks, there lived a dragon named Ember. Unlike others of her kind, Ember cherished solitude over treasure, and the songs of the stream over the roar of flames. One misty dawn, a young shepherd stumbled into her sanctuary, lost and frightened. \n", + "\n", + "Instead of fury, he was met with kindness as Ember extended a wing, guiding him back to safety. In gratitude, the shepherd visited yearly, bringing tales of his world beyond the mountains. Over time, a friendship blossomed, binding man and dragon in shared stories and laughter.\n", + "\n", + "As the years passed, the legend of Ember the gentle-hearted spread far and wide, forever changing the way dragons were seen in the hearts of many.\n", + "\n", + "\n", + "------------\n", + "\n", + "The token usage was:\n", + "RequestUsage(prompt_tokens=17, completion_tokens=146)\n" + ] + } + ], + "source": [ + "messages = [\n", + " UserMessage(content=\"Write a very short story about a dragon.\", source=\"user\"),\n", + "]\n", + "\n", + "# Create a stream.\n", + "stream = model_client.create_stream(messages=messages, extra_create_args={\"stream_options\": {\"include_usage\": True}})\n", + "\n", + "# Iterate over the stream and print the responses.\n", + "print(\"Streamed responses:\")\n", + "async for response in stream: # type: ignore\n", + " if isinstance(response, str):\n", + " # A partial response is a string.\n", + " print(response, flush=True, end=\"\")\n", + " else:\n", + " # The last response is a CreateResult object with the complete message.\n", + " print(\"\\n\\n------------\\n\")\n", + " print(\"The complete response:\", flush=True)\n", + " print(response.content, flush=True)\n", + " print(\"\\n\\n------------\\n\")\n", + " print(\"The token usage was:\", flush=True)\n", + " print(response.usage, flush=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Azure OpenAI\n", + "\n", + "To use the {py:class}`~autogen_ext.models.AzureOpenAIChatCompletionClient`, you need to provide\n", + "the deployment id, Azure Cognitive Services endpoint, api version, and model capabilities.\n", + "For authentication, you can either provide an API key or an Azure Active Directory (AAD) token credential.\n", + "To use AAD authentication, you need to first install the `azure-identity` package." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "vscode": { + "languageId": "shellscript" + } + }, + "outputs": [], + "source": [ + "# pip install azure-identity" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The following code snippet shows how to use AAD authentication.\n", + "The identity used must be assigned the [**Cognitive Services OpenAI User**](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/role-based-access-control#cognitive-services-openai-user) role." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from autogen_ext.models import AzureOpenAIChatCompletionClient\n", + "from azure.identity import DefaultAzureCredential, get_bearer_token_provider\n", + "\n", + "# Create the token provider\n", + "token_provider = get_bearer_token_provider(DefaultAzureCredential(), \"https://cognitiveservices.azure.com/.default\")\n", + "\n", + "az_model_client = AzureOpenAIChatCompletionClient(\n", + " azure_deployment=\"{your-azure-deployment}\",\n", + " model=\"{model-name, such as gpt-4o}\",\n", + " api_version=\"2024-06-01\",\n", + " azure_endpoint=\"https://{your-custom-endpoint}.openai.azure.com/\",\n", + " azure_ad_token_provider=token_provider, # Optional if you choose key-based authentication.\n", + " # api_key=\"sk-...\", # For key-based authentication.\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```{note}\n", + "See [here](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/managed-identity#chat-completions) for how to use the Azure client directly or for more info.\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Build Agent using Model Client\n", + "\n", + "Let's create a simple AI agent that can respond to messages using the ChatCompletion API." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "from dataclasses import dataclass\n", + "\n", + "from autogen_core import MessageContext, RoutedAgent, message_handler\n", + "from autogen_core.application import SingleThreadedAgentRuntime\n", + "from autogen_core.components.models import ChatCompletionClient, SystemMessage, UserMessage\n", + "from autogen_ext.models import OpenAIChatCompletionClient\n", + "\n", + "\n", + "@dataclass\n", + "class Message:\n", + " content: str\n", + "\n", + "\n", + "class SimpleAgent(RoutedAgent):\n", + " def __init__(self, model_client: ChatCompletionClient) -> None:\n", + " super().__init__(\"A simple agent\")\n", + " self._system_messages = [SystemMessage(content=\"You are a helpful AI assistant.\")]\n", + " self._model_client = model_client\n", + "\n", + " @message_handler\n", + " async def handle_user_message(self, message: Message, ctx: MessageContext) -> Message:\n", + " # Prepare input to the chat completion model.\n", + " user_message = UserMessage(content=message.content, source=\"user\")\n", + " response = await self._model_client.create(\n", + " self._system_messages + [user_message], cancellation_token=ctx.cancellation_token\n", + " )\n", + " # Return with the model's response.\n", + " assert isinstance(response.content, str)\n", + " return Message(content=response.content)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `SimpleAgent` class is a subclass of the\n", + "{py:class}`autogen_core.components.RoutedAgent` class for the convenience of automatically routing messages to the appropriate handlers.\n", + "It has a single handler, `handle_user_message`, which handles message from the user. It uses the `ChatCompletionClient` to generate a response to the message.\n", + "It then returns the response to the user, following the direct communication model.\n", + "\n", + "```{note}\n", + "The `cancellation_token` of the type {py:class}`autogen_core.base.CancellationToken` is used to cancel\n", + "asynchronous operations. It is linked to async calls inside the message handlers\n", + "and can be used by the caller to cancel the handlers.\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Seattle is a vibrant city with a wide range of activities and attractions. Here are some fun things to do in Seattle:\n", + "\n", + "1. **Space Needle**: Visit this iconic observation tower for stunning views of the city and surrounding mountains.\n", + "\n", + "2. **Pike Place Market**: Explore this historic market where you can see the famous fish toss, buy local produce, and find unique crafts and eateries.\n", + "\n", + "3. **Museum of Pop Culture (MoPOP)**: Dive into the world of contemporary culture, music, and science fiction at this interactive museum.\n", + "\n", + "4. **Chihuly Garden and Glass**: Marvel at the beautiful glass art installations by artist Dale Chihuly, located right next to the Space Needle.\n", + "\n", + "5. **Seattle Aquarium**: Discover the diverse marine life of the Pacific Northwest at this engaging aquarium.\n", + "\n", + "6. **Seattle Art Museum**: Explore a vast collection of art from around the world, including contemporary and indigenous art.\n", + "\n", + "7. **Kerry Park**: For one of the best views of the Seattle skyline, head to this small park on Queen Anne Hill.\n", + "\n", + "8. **Ballard Locks**: Watch boats pass through the locks and observe the salmon ladder to see salmon migrating.\n", + "\n", + "9. **Ferry to Bainbridge Island**: Take a scenic ferry ride across Puget Sound to enjoy charming shops, restaurants, and beautiful natural scenery.\n", + "\n", + "10. **Olympic Sculpture Park**: Stroll through this outdoor park with large-scale sculptures and stunning views of the waterfront and mountains.\n", + "\n", + "11. **Underground Tour**: Discover Seattle's history on this quirky tour of the city's underground passageways in Pioneer Square.\n", + "\n", + "12. **Seattle Waterfront**: Enjoy the shops, restaurants, and attractions along the waterfront, including the Seattle Great Wheel and the aquarium.\n", + "\n", + "13. **Discovery Park**: Explore the largest green space in Seattle, featuring trails, beaches, and views of Puget Sound.\n", + "\n", + "14. **Food Tours**: Try out Seattle’s diverse culinary scene, including fresh seafood, international cuisines, and coffee culture (don’t miss the original Starbucks!).\n", + "\n", + "15. **Attend a Sports Game**: Catch a Seahawks (NFL), Mariners (MLB), or Sounders (MLS) game for a lively local experience.\n", + "\n", + "Whether you're interested in culture, nature, food, or history, Seattle has something for everyone to enjoy!\n" + ] + } + ], + "source": [ + "# Create the runtime and register the agent.\n", + "from autogen_core import AgentId\n", + "\n", + "runtime = SingleThreadedAgentRuntime()\n", + "await SimpleAgent.register(\n", + " runtime,\n", + " \"simple_agent\",\n", + " lambda: SimpleAgent(\n", + " OpenAIChatCompletionClient(\n", + " model=\"gpt-4o-mini\",\n", + " # api_key=\"sk-...\", # Optional if you have an OPENAI_API_KEY set in the environment.\n", + " )\n", + " ),\n", + ")\n", + "# Start the runtime processing messages.\n", + "runtime.start()\n", + "# Send a message to the agent and get the response.\n", + "message = Message(\"Hello, what are some fun things to do in Seattle?\")\n", + "response = await runtime.send_message(message, AgentId(\"simple_agent\", \"default\"))\n", + "print(response.content)\n", + "# Stop the runtime processing messages.\n", + "await runtime.stop()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Manage Model Context\n", + "\n", + "The above `SimpleAgent` always responds with a fresh context that contains only\n", + "the system message and the latest user's message.\n", + "We can use model context classes from {py:mod}`autogen_core.components.model_context`\n", + "to make the agent \"remember\" previous conversations.\n", + "A model context supports storage and retrieval of Chat Completion messages.\n", + "It is always used together with a model client to generate LLM-based responses.\n", + "\n", + "For example, {py:mod}`~autogen_core.components.model_context.BufferedChatCompletionContext`\n", + "is a most-recent-used (MRU) context that stores the most recent `buffer_size`\n", + "number of messages. This is useful to avoid context overflow in many LLMs.\n", + "\n", + "Let's update the previous example to use\n", + "{py:mod}`~autogen_core.components.model_context.BufferedChatCompletionContext`." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "from autogen_core.components.model_context import BufferedChatCompletionContext\n", + "from autogen_core.components.models import AssistantMessage\n", + "\n", + "\n", + "class SimpleAgentWithContext(RoutedAgent):\n", + " def __init__(self, model_client: ChatCompletionClient) -> None:\n", + " super().__init__(\"A simple agent\")\n", + " self._system_messages = [SystemMessage(content=\"You are a helpful AI assistant.\")]\n", + " self._model_client = model_client\n", + " self._model_context = BufferedChatCompletionContext(buffer_size=5)\n", + "\n", + " @message_handler\n", + " async def handle_user_message(self, message: Message, ctx: MessageContext) -> Message:\n", + " # Prepare input to the chat completion model.\n", + " user_message = UserMessage(content=message.content, source=\"user\")\n", + " # Add message to model context.\n", + " await self._model_context.add_message(user_message)\n", + " # Generate a response.\n", + " response = await self._model_client.create(\n", + " self._system_messages + (await self._model_context.get_messages()),\n", + " cancellation_token=ctx.cancellation_token,\n", + " )\n", + " # Return with the model's response.\n", + " assert isinstance(response.content, str)\n", + " # Add message to model context.\n", + " await self._model_context.add_message(AssistantMessage(content=response.content, source=self.metadata[\"type\"]))\n", + " return Message(content=response.content)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's try to ask follow up questions after the first one." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Question: Hello, what are some fun things to do in Seattle?\n", + "Response: Seattle offers a wide variety of fun activities and attractions for visitors. Here are some highlights:\n", + "\n", + "1. **Pike Place Market**: Explore this iconic market, where you can find fresh produce, unique crafts, and the famous fish-throwing vendors. Don’t forget to visit the original Starbucks!\n", + "\n", + "2. **Space Needle**: Enjoy breathtaking views of the city and Mount Rainier from the observation deck of this iconic structure. You can also dine at the SkyCity restaurant.\n", + "\n", + "3. **Chihuly Garden and Glass**: Admire the stunning glass art installations created by artist Dale Chihuly. The garden and exhibit are particularly beautiful, especially in good weather.\n", + "\n", + "4. **Museum of Pop Culture (MoPOP)**: Dive into the world of music, science fiction, and pop culture through interactive exhibits and memorabilia.\n", + "\n", + "5. **Seattle Aquarium**: Located on the waterfront, the aquarium features a variety of marine life native to the Pacific Northwest, including otters and diving birds.\n", + "\n", + "6. **Seattle Art Museum (SAM)**: Explore a diverse collection of art from around the world, including Native American art and contemporary pieces.\n", + "\n", + "7. **Ballard Locks**: Watch boats travel between the Puget Sound and Lake Union, and see salmon navigating the fish ladder during spawning season.\n", + "\n", + "8. **Fremont Troll**: Visit this quirky public art installation located under the Aurora Bridge, where you can take fun photos with the giant troll.\n", + "\n", + "9. **Kerry Park**: For a picturesque view of the Seattle skyline, head to Kerry Park on Queen Anne Hill, especially at sunset.\n", + "\n", + "10. **Take a Ferry Ride**: Enjoy the scenic views while taking a ferry to nearby Bainbridge Island or Vashon Island for a relaxing day trip.\n", + "\n", + "11. **Underground Tour**: Explore Seattle’s history on an entertaining underground tour in Pioneer Square, where you’ll learn about the city’s early days.\n", + "\n", + "12. **Attend a Sporting Event**: Depending on the season, catch a Seattle Seahawks (NFL) game, a Seattle Mariners (MLB) game, or a Seattle Sounders (MLS) match.\n", + "\n", + "13. **Explore Discovery Park**: Enjoy nature with hiking trails, beach access, and stunning views of the Puget Sound and Olympic Mountains.\n", + "\n", + "14. **West Seattle’s Alki Beach**: Relax at this beach with beautiful views of the Seattle skyline and enjoy beachside activities like biking or kayaking.\n", + "\n", + "15. **Dining and Craft Beer**: Seattle has a vibrant food scene and is known for its seafood, coffee culture, and craft breweries. Make sure to explore local restaurants and breweries.\n", + "\n", + "There’s something for everyone in Seattle, whether you’re interested in nature, art, history, or food!\n", + "-----\n", + "Question: What was the first thing you mentioned?\n", + "Response: The first thing I mentioned was **Pike Place Market**, an iconic market in Seattle where you can find fresh produce, unique crafts, and experience the famous fish-throwing vendors. It's also home to the original Starbucks and various charming shops and eateries.\n" + ] + } + ], + "source": [ + "runtime = SingleThreadedAgentRuntime()\n", + "await SimpleAgentWithContext.register(\n", + " runtime,\n", + " \"simple_agent_context\",\n", + " lambda: SimpleAgentWithContext(\n", + " OpenAIChatCompletionClient(\n", + " model=\"gpt-4o-mini\",\n", + " # api_key=\"sk-...\", # Optional if you have an OPENAI_API_KEY set in the environment.\n", + " )\n", + " ),\n", + ")\n", + "# Start the runtime processing messages.\n", + "runtime.start()\n", + "agent_id = AgentId(\"simple_agent_context\", \"default\")\n", + "\n", + "# First question.\n", + "message = Message(\"Hello, what are some fun things to do in Seattle?\")\n", + "print(f\"Question: {message.content}\")\n", + "response = await runtime.send_message(message, agent_id)\n", + "print(f\"Response: {response.content}\")\n", + "print(\"-----\")\n", + "\n", + "# Second question.\n", + "message = Message(\"What was the first thing you mentioned?\")\n", + "print(f\"Question: {message.content}\")\n", + "response = await runtime.send_message(message, agent_id)\n", + "print(f\"Response: {response.content}\")\n", + "\n", + "# Stop the runtime processing messages.\n", + "await runtime.stop()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "From the second response, you can see the agent now can recall its own previous responses." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/tools.ipynb b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/tools.ipynb index b7859dfc63f1..a5ddf267ae83 100644 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/tools.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/tools.ipynb @@ -1,315 +1,315 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Tools\n", - "\n", - "Tools are code that can be executed by an agent to perform actions. A tool\n", - "can be a simple function such as a calculator, or an API call to a third-party service\n", - "such as stock price lookup or weather forecast.\n", - "In the context of AI agents, tools are designed to be executed by agents in\n", - "response to model-generated function calls.\n", - "\n", - "AutoGen provides the {py:mod}`autogen_core.components.tools` module with a suite of built-in\n", - "tools and utilities for creating and running custom tools." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Built-in Tools\n", - "\n", - "One of the built-in tools is the {py:class}`~autogen_core.components.tools.PythonCodeExecutionTool`,\n", - "which allows agents to execute Python code snippets.\n", - "\n", - "Here is how you create the tool and use it." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Hello, world!\n", - "\n" - ] - } - ], - "source": [ - "from autogen_core import CancellationToken\n", - "from autogen_core.components.tools import PythonCodeExecutionTool\n", - "from autogen_ext.code_executors import DockerCommandLineCodeExecutor\n", - "\n", - "# Create the tool.\n", - "code_executor = DockerCommandLineCodeExecutor()\n", - "await code_executor.start()\n", - "code_execution_tool = PythonCodeExecutionTool(code_executor)\n", - "cancellation_token = CancellationToken()\n", - "\n", - "# Use the tool directly without an agent.\n", - "code = \"print('Hello, world!')\"\n", - "result = await code_execution_tool.run_json({\"code\": code}, cancellation_token)\n", - "print(code_execution_tool.return_value_as_string(result))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The {py:class}`~autogen_core.components.code_executor.docker_executorCommandLineCodeExecutor`\n", - "class is a built-in code executor that runs Python code snippets in a subprocess\n", - "in the local command line environment.\n", - "The {py:class}`~autogen_core.components.tools.PythonCodeExecutionTool` class wraps the code executor\n", - "and provides a simple interface to execute Python code snippets.\n", - "\n", - "Other built-in tools will be added in the future." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Custom Function Tools\n", - "\n", - "A tool can also be a simple Python function that performs a specific action.\n", - "To create a custom function tool, you just need to create a Python function\n", - "and use the {py:class}`~autogen_core.components.tools.FunctionTool` class to wrap it.\n", - "\n", - "The {py:class}`~autogen_core.components.tools.FunctionTool` class uses descriptions and type annotations\n", - "to inform the LLM when and how to use a given function. The description provides context\n", - "about the function’s purpose and intended use cases, while type annotations inform the LLM about\n", - "the expected parameters and return type.\n", - "\n", - "For example, a simple tool to obtain the stock price of a company might look like this:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "80.44429939059668\n" - ] - } - ], - "source": [ - "import random\n", - "\n", - "from autogen_core import CancellationToken\n", - "from autogen_core.components.tools import FunctionTool\n", - "from typing_extensions import Annotated\n", - "\n", - "\n", - "async def get_stock_price(ticker: str, date: Annotated[str, \"Date in YYYY/MM/DD\"]) -> float:\n", - " # Returns a random stock price for demonstration purposes.\n", - " return random.uniform(10, 200)\n", - "\n", - "\n", - "# Create a function tool.\n", - "stock_price_tool = FunctionTool(get_stock_price, description=\"Get the stock price.\")\n", - "\n", - "# Run the tool.\n", - "cancellation_token = CancellationToken()\n", - "result = await stock_price_tool.run_json({\"ticker\": \"AAPL\", \"date\": \"2021/01/01\"}, cancellation_token)\n", - "\n", - "# Print the result.\n", - "print(stock_price_tool.return_value_as_string(result))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Tool-Equipped Agent\n", - "\n", - "To use tools with an agent, you can use {py:class}`~autogen_core.components.tool_agent.ToolAgent`,\n", - "by using it in a composition pattern.\n", - "Here is an example tool-use agent that uses {py:class}`~autogen_core.components.tool_agent.ToolAgent`\n", - "as an inner agent for executing tools." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "from dataclasses import dataclass\n", - "from typing import List\n", - "\n", - "from autogen_core import AgentId, AgentInstantiationContext, MessageContext, RoutedAgent, message_handler\n", - "from autogen_core.application import SingleThreadedAgentRuntime\n", - "from autogen_core.components.models import (\n", - " ChatCompletionClient,\n", - " LLMMessage,\n", - " SystemMessage,\n", - " UserMessage,\n", - ")\n", - "from autogen_core.components.tools import FunctionTool, Tool, ToolSchema\n", - "from autogen_core.tool_agent import ToolAgent, tool_agent_caller_loop\n", - "from autogen_ext.models import OpenAIChatCompletionClient\n", - "\n", - "\n", - "@dataclass\n", - "class Message:\n", - " content: str\n", - "\n", - "\n", - "class ToolUseAgent(RoutedAgent):\n", - " def __init__(self, model_client: ChatCompletionClient, tool_schema: List[ToolSchema], tool_agent_type: str) -> None:\n", - " super().__init__(\"An agent with tools\")\n", - " self._system_messages: List[LLMMessage] = [SystemMessage(\"You are a helpful AI assistant.\")]\n", - " self._model_client = model_client\n", - " self._tool_schema = tool_schema\n", - " self._tool_agent_id = AgentId(tool_agent_type, self.id.key)\n", - "\n", - " @message_handler\n", - " async def handle_user_message(self, message: Message, ctx: MessageContext) -> Message:\n", - " # Create a session of messages.\n", - " session: List[LLMMessage] = [UserMessage(content=message.content, source=\"user\")]\n", - " # Run the caller loop to handle tool calls.\n", - " messages = await tool_agent_caller_loop(\n", - " self,\n", - " tool_agent_id=self._tool_agent_id,\n", - " model_client=self._model_client,\n", - " input_messages=session,\n", - " tool_schema=self._tool_schema,\n", - " cancellation_token=ctx.cancellation_token,\n", - " )\n", - " # Return the final response.\n", - " assert isinstance(messages[-1].content, str)\n", - " return Message(content=messages[-1].content)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The `ToolUseAgent` class uses a convenience function {py:meth}`~autogen_core.components.tool_agent.tool_agent_caller_loop`, \n", - "to handle the interaction between the model and the tool agent.\n", - "The core idea can be described using a simple control flow graph:\n", - "\n", - "![ToolUseAgent control flow graph](tool-use-agent-cfg.svg)\n", - "\n", - "The `ToolUseAgent`'s `handle_user_message` handler handles messages from the user,\n", - "and determines whether the model has generated a tool call.\n", - "If the model has generated tool calls, then the handler sends a function call\n", - "message to the {py:class}`~autogen_core.components.tool_agent.ToolAgent` agent\n", - "to execute the tools,\n", - "and then queries the model again with the results of the tool calls.\n", - "This process continues until the model stops generating tool calls,\n", - "at which point the final response is returned to the user.\n", - "\n", - "By having the tool execution logic in a separate agent,\n", - "we expose the model-tool interactions to the agent runtime as messages, so the tool executions\n", - "can be observed externally and intercepted if necessary.\n", - "\n", - "To run the agent, we need to create a runtime and register the agent." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "AgentType(type='tool_use_agent')" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Create a runtime.\n", - "runtime = SingleThreadedAgentRuntime()\n", - "# Create the tools.\n", - "tools: List[Tool] = [FunctionTool(get_stock_price, description=\"Get the stock price.\")]\n", - "# Register the agents.\n", - "await ToolAgent.register(runtime, \"tool_executor_agent\", lambda: ToolAgent(\"tool executor agent\", tools))\n", - "await ToolUseAgent.register(\n", - " runtime,\n", - " \"tool_use_agent\",\n", - " lambda: ToolUseAgent(\n", - " OpenAIChatCompletionClient(model=\"gpt-4o-mini\"), [tool.schema for tool in tools], \"tool_executor_agent\"\n", - " ),\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This example uses the {py:class}`autogen_core.components.models.OpenAIChatCompletionClient`,\n", - "for Azure OpenAI and other clients, see [Model Clients](./model-clients.ipynb).\n", - "Let's test the agent with a question about stock price." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The stock price of NVDA (NVIDIA Corporation) on June 1, 2024, was approximately $179.46.\n" - ] - } - ], - "source": [ - "# Start processing messages.\n", - "runtime.start()\n", - "# Send a direct message to the tool agent.\n", - "tool_use_agent = AgentId(\"tool_use_agent\", \"default\")\n", - "response = await runtime.send_message(Message(\"What is the stock price of NVDA on 2024/06/01?\"), tool_use_agent)\n", - "print(response.content)\n", - "# Stop processing messages.\n", - "await runtime.stop()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "autogen_core", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.7" - } - }, - "nbformat": 4, - "nbformat_minor": 2 + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Tools\n", + "\n", + "Tools are code that can be executed by an agent to perform actions. A tool\n", + "can be a simple function such as a calculator, or an API call to a third-party service\n", + "such as stock price lookup or weather forecast.\n", + "In the context of AI agents, tools are designed to be executed by agents in\n", + "response to model-generated function calls.\n", + "\n", + "AutoGen provides the {py:mod}`autogen_core.components.tools` module with a suite of built-in\n", + "tools and utilities for creating and running custom tools." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Built-in Tools\n", + "\n", + "One of the built-in tools is the {py:class}`~autogen_core.components.tools.PythonCodeExecutionTool`,\n", + "which allows agents to execute Python code snippets.\n", + "\n", + "Here is how you create the tool and use it." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Hello, world!\n", + "\n" + ] + } + ], + "source": [ + "from autogen_core import CancellationToken\n", + "from autogen_core.components.tools import PythonCodeExecutionTool\n", + "from autogen_ext.code_executors import DockerCommandLineCodeExecutor\n", + "\n", + "# Create the tool.\n", + "code_executor = DockerCommandLineCodeExecutor()\n", + "await code_executor.start()\n", + "code_execution_tool = PythonCodeExecutionTool(code_executor)\n", + "cancellation_token = CancellationToken()\n", + "\n", + "# Use the tool directly without an agent.\n", + "code = \"print('Hello, world!')\"\n", + "result = await code_execution_tool.run_json({\"code\": code}, cancellation_token)\n", + "print(code_execution_tool.return_value_as_string(result))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The {py:class}`~autogen_core.components.code_executor.docker_executorCommandLineCodeExecutor`\n", + "class is a built-in code executor that runs Python code snippets in a subprocess\n", + "in the local command line environment.\n", + "The {py:class}`~autogen_core.components.tools.PythonCodeExecutionTool` class wraps the code executor\n", + "and provides a simple interface to execute Python code snippets.\n", + "\n", + "Other built-in tools will be added in the future." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Custom Function Tools\n", + "\n", + "A tool can also be a simple Python function that performs a specific action.\n", + "To create a custom function tool, you just need to create a Python function\n", + "and use the {py:class}`~autogen_core.components.tools.FunctionTool` class to wrap it.\n", + "\n", + "The {py:class}`~autogen_core.components.tools.FunctionTool` class uses descriptions and type annotations\n", + "to inform the LLM when and how to use a given function. The description provides context\n", + "about the function’s purpose and intended use cases, while type annotations inform the LLM about\n", + "the expected parameters and return type.\n", + "\n", + "For example, a simple tool to obtain the stock price of a company might look like this:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "80.44429939059668\n" + ] + } + ], + "source": [ + "import random\n", + "\n", + "from autogen_core import CancellationToken\n", + "from autogen_core.components.tools import FunctionTool\n", + "from typing_extensions import Annotated\n", + "\n", + "\n", + "async def get_stock_price(ticker: str, date: Annotated[str, \"Date in YYYY/MM/DD\"]) -> float:\n", + " # Returns a random stock price for demonstration purposes.\n", + " return random.uniform(10, 200)\n", + "\n", + "\n", + "# Create a function tool.\n", + "stock_price_tool = FunctionTool(get_stock_price, description=\"Get the stock price.\")\n", + "\n", + "# Run the tool.\n", + "cancellation_token = CancellationToken()\n", + "result = await stock_price_tool.run_json({\"ticker\": \"AAPL\", \"date\": \"2021/01/01\"}, cancellation_token)\n", + "\n", + "# Print the result.\n", + "print(stock_price_tool.return_value_as_string(result))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Tool-Equipped Agent\n", + "\n", + "To use tools with an agent, you can use {py:class}`~autogen_core.components.tool_agent.ToolAgent`,\n", + "by using it in a composition pattern.\n", + "Here is an example tool-use agent that uses {py:class}`~autogen_core.components.tool_agent.ToolAgent`\n", + "as an inner agent for executing tools." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from dataclasses import dataclass\n", + "from typing import List\n", + "\n", + "from autogen_core import AgentId, AgentInstantiationContext, MessageContext, RoutedAgent, message_handler\n", + "from autogen_core.application import SingleThreadedAgentRuntime\n", + "from autogen_core.components.models import (\n", + " ChatCompletionClient,\n", + " LLMMessage,\n", + " SystemMessage,\n", + " UserMessage,\n", + ")\n", + "from autogen_core.components.tools import FunctionTool, Tool, ToolSchema\n", + "from autogen_core.tool_agent import ToolAgent, tool_agent_caller_loop\n", + "from autogen_ext.models import OpenAIChatCompletionClient\n", + "\n", + "\n", + "@dataclass\n", + "class Message:\n", + " content: str\n", + "\n", + "\n", + "class ToolUseAgent(RoutedAgent):\n", + " def __init__(self, model_client: ChatCompletionClient, tool_schema: List[ToolSchema], tool_agent_type: str) -> None:\n", + " super().__init__(\"An agent with tools\")\n", + " self._system_messages: List[LLMMessage] = [SystemMessage(content=\"You are a helpful AI assistant.\")]\n", + " self._model_client = model_client\n", + " self._tool_schema = tool_schema\n", + " self._tool_agent_id = AgentId(tool_agent_type, self.id.key)\n", + "\n", + " @message_handler\n", + " async def handle_user_message(self, message: Message, ctx: MessageContext) -> Message:\n", + " # Create a session of messages.\n", + " session: List[LLMMessage] = [UserMessage(content=message.content, source=\"user\")]\n", + " # Run the caller loop to handle tool calls.\n", + " messages = await tool_agent_caller_loop(\n", + " self,\n", + " tool_agent_id=self._tool_agent_id,\n", + " model_client=self._model_client,\n", + " input_messages=session,\n", + " tool_schema=self._tool_schema,\n", + " cancellation_token=ctx.cancellation_token,\n", + " )\n", + " # Return the final response.\n", + " assert isinstance(messages[-1].content, str)\n", + " return Message(content=messages[-1].content)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `ToolUseAgent` class uses a convenience function {py:meth}`~autogen_core.components.tool_agent.tool_agent_caller_loop`, \n", + "to handle the interaction between the model and the tool agent.\n", + "The core idea can be described using a simple control flow graph:\n", + "\n", + "![ToolUseAgent control flow graph](tool-use-agent-cfg.svg)\n", + "\n", + "The `ToolUseAgent`'s `handle_user_message` handler handles messages from the user,\n", + "and determines whether the model has generated a tool call.\n", + "If the model has generated tool calls, then the handler sends a function call\n", + "message to the {py:class}`~autogen_core.components.tool_agent.ToolAgent` agent\n", + "to execute the tools,\n", + "and then queries the model again with the results of the tool calls.\n", + "This process continues until the model stops generating tool calls,\n", + "at which point the final response is returned to the user.\n", + "\n", + "By having the tool execution logic in a separate agent,\n", + "we expose the model-tool interactions to the agent runtime as messages, so the tool executions\n", + "can be observed externally and intercepted if necessary.\n", + "\n", + "To run the agent, we need to create a runtime and register the agent." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AgentType(type='tool_use_agent')" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create a runtime.\n", + "runtime = SingleThreadedAgentRuntime()\n", + "# Create the tools.\n", + "tools: List[Tool] = [FunctionTool(get_stock_price, description=\"Get the stock price.\")]\n", + "# Register the agents.\n", + "await ToolAgent.register(runtime, \"tool_executor_agent\", lambda: ToolAgent(\"tool executor agent\", tools))\n", + "await ToolUseAgent.register(\n", + " runtime,\n", + " \"tool_use_agent\",\n", + " lambda: ToolUseAgent(\n", + " OpenAIChatCompletionClient(model=\"gpt-4o-mini\"), [tool.schema for tool in tools], \"tool_executor_agent\"\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This example uses the {py:class}`autogen_core.components.models.OpenAIChatCompletionClient`,\n", + "for Azure OpenAI and other clients, see [Model Clients](./model-clients.ipynb).\n", + "Let's test the agent with a question about stock price." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The stock price of NVDA (NVIDIA Corporation) on June 1, 2024, was approximately $179.46.\n" + ] + } + ], + "source": [ + "# Start processing messages.\n", + "runtime.start()\n", + "# Send a direct message to the tool agent.\n", + "tool_use_agent = AgentId(\"tool_use_agent\", \"default\")\n", + "response = await runtime.send_message(Message(\"What is the stock price of NVDA on 2024/06/01?\"), tool_use_agent)\n", + "print(response.content)\n", + "# Stop processing messages.\n", + "await runtime.stop()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "autogen_core", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/python/packages/autogen-core/samples/common/patterns/_group_chat_utils.py b/python/packages/autogen-core/samples/common/patterns/_group_chat_utils.py index b87fdc8cae0b..e0ad173ce196 100644 --- a/python/packages/autogen-core/samples/common/patterns/_group_chat_utils.py +++ b/python/packages/autogen-core/samples/common/patterns/_group_chat_utils.py @@ -36,7 +36,7 @@ async def select_speaker(context: ChatCompletionContext, client: ChatCompletionC Read the above conversation. Then select the next role from {participants} to play. Only return the role. """ - select_speaker_messages = [SystemMessage(select_speaker_prompt)] + select_speaker_messages = [SystemMessage(content=select_speaker_prompt)] response = await client.create(messages=select_speaker_messages) assert isinstance(response.content, str) mentions = await mentioned_agents(response.content, agents) diff --git a/python/packages/autogen-core/samples/common/utils.py b/python/packages/autogen-core/samples/common/utils.py index 0765ceec561a..7d1cdad1c2f9 100644 --- a/python/packages/autogen-core/samples/common/utils.py +++ b/python/packages/autogen-core/samples/common/utils.py @@ -92,7 +92,7 @@ def convert_messages_to_llm_messages( converted_message_2 = convert_content_message_to_user_message(message, handle_unrepresentable) if converted_message_2 is not None: result.append(converted_message_2) - case FunctionExecutionResultMessage(_): + case FunctionExecutionResultMessage(content=_): converted_message_3 = convert_tool_call_response_message(message, handle_unrepresentable) if converted_message_3 is not None: result.append(converted_message_3) diff --git a/python/packages/autogen-core/samples/distributed-group-chat/_agents.py b/python/packages/autogen-core/samples/distributed-group-chat/_agents.py index ce0547775878..f819bf7266a3 100644 --- a/python/packages/autogen-core/samples/distributed-group-chat/_agents.py +++ b/python/packages/autogen-core/samples/distributed-group-chat/_agents.py @@ -31,7 +31,7 @@ def __init__( super().__init__(description=description) self._group_chat_topic_type = group_chat_topic_type self._model_client = model_client - self._system_message = SystemMessage(system_message) + self._system_message = SystemMessage(content=system_message) self._chat_history: List[LLMMessage] = [] self._ui_config = ui_config self.console = Console() @@ -126,7 +126,7 @@ async def handle_message(self, message: GroupChatMessage, ctx: MessageContext) - Read the above conversation. Then select the next role from {participants} to play. if you think it's enough talking (for example they have talked for {self._max_rounds} rounds), return 'FINISH'. """ - system_message = SystemMessage(selector_prompt) + system_message = SystemMessage(content=selector_prompt) completion = await self._model_client.create([system_message], cancellation_token=ctx.cancellation_token) assert isinstance( diff --git a/python/packages/autogen-core/samples/slow_human_in_loop.py b/python/packages/autogen-core/samples/slow_human_in_loop.py index e973717c94d8..50f793e2f360 100644 --- a/python/packages/autogen-core/samples/slow_human_in_loop.py +++ b/python/packages/autogen-core/samples/slow_human_in_loop.py @@ -160,12 +160,14 @@ def __init__( self._name = name self._model_client = model_client self._system_messages = [ - SystemMessage(f""" + SystemMessage( + content=f""" I am a helpful AI assistant that helps schedule meetings. If there are missing parameters, I will ask for them. Today's date is {datetime.datetime.now().strftime("%Y-%m-%d")} -""") +""" + ) ] @message_handler diff --git a/python/packages/autogen-core/src/autogen_core/_closure_agent.py b/python/packages/autogen-core/src/autogen_core/_closure_agent.py index 9a5e1a2c6a26..03206d18feff 100644 --- a/python/packages/autogen-core/src/autogen_core/_closure_agent.py +++ b/python/packages/autogen-core/src/autogen_core/_closure_agent.py @@ -116,10 +116,12 @@ async def on_message_impl(self, message: Any, ctx: MessageContext) -> Any: return await self._closure(self, message, ctx) async def save_state(self) -> Mapping[str, Any]: - raise ValueError("save_state not implemented for ClosureAgent") + """Closure agents do not have state. So this method always returns an empty dictionary.""" + return {} async def load_state(self, state: Mapping[str, Any]) -> None: - raise ValueError("load_state not implemented for ClosureAgent") + """Closure agents do not have state. So this method does nothing.""" + pass @classmethod async def register_closure( diff --git a/python/packages/autogen-core/src/autogen_core/components/models/_types.py b/python/packages/autogen-core/src/autogen_core/components/models/_types.py index 3bc047d277fa..6ab79d0ab3d0 100644 --- a/python/packages/autogen-core/src/autogen_core/components/models/_types.py +++ b/python/packages/autogen-core/src/autogen_core/components/models/_types.py @@ -1,42 +1,49 @@ from dataclasses import dataclass from typing import List, Literal, Optional, Union +from pydantic import BaseModel, Field +from typing_extensions import Annotated + from ... import FunctionCall, Image -@dataclass -class SystemMessage: +class SystemMessage(BaseModel): content: str + type: Literal["SystemMessage"] = "SystemMessage" -@dataclass -class UserMessage: +class UserMessage(BaseModel): content: Union[str, List[Union[str, Image]]] # Name of the agent that sent this message source: str + type: Literal["UserMessage"] = "UserMessage" -@dataclass -class AssistantMessage: + +class AssistantMessage(BaseModel): content: Union[str, List[FunctionCall]] # Name of the agent that sent this message source: str + type: Literal["AssistantMessage"] = "AssistantMessage" -@dataclass -class FunctionExecutionResult: + +class FunctionExecutionResult(BaseModel): content: str call_id: str -@dataclass -class FunctionExecutionResultMessage: +class FunctionExecutionResultMessage(BaseModel): content: List[FunctionExecutionResult] + type: Literal["FunctionExecutionResultMessage"] = "FunctionExecutionResultMessage" + -LLMMessage = Union[SystemMessage, UserMessage, AssistantMessage, FunctionExecutionResultMessage] +LLMMessage = Annotated[ + Union[SystemMessage, UserMessage, AssistantMessage, FunctionExecutionResultMessage], Field(discriminator="type") +] @dataclass @@ -54,16 +61,14 @@ class TopLogprob: bytes: Optional[List[int]] = None -@dataclass -class ChatCompletionTokenLogprob: +class ChatCompletionTokenLogprob(BaseModel): token: str logprob: float top_logprobs: Optional[List[TopLogprob] | None] = None bytes: Optional[List[int]] = None -@dataclass -class CreateResult: +class CreateResult(BaseModel): finish_reason: FinishReasons content: Union[str, List[FunctionCall]] usage: RequestUsage diff --git a/python/packages/autogen-ext/src/autogen_ext/agents/file_surfer/_file_surfer.py b/python/packages/autogen-ext/src/autogen_ext/agents/file_surfer/_file_surfer.py index bd0988c5d00e..cc60bd8ee42e 100644 --- a/python/packages/autogen-ext/src/autogen_ext/agents/file_surfer/_file_surfer.py +++ b/python/packages/autogen-ext/src/autogen_ext/agents/file_surfer/_file_surfer.py @@ -36,9 +36,11 @@ class FileSurfer(BaseChatAgent): DEFAULT_DESCRIPTION = "An agent that can handle local files." DEFAULT_SYSTEM_MESSAGES = [ - SystemMessage(""" + SystemMessage( + content=""" You are a helpful AI Assistant. - When given a user query, use available functions to help the user with their request."""), + When given a user query, use available functions to help the user with their request.""" + ), ] def __init__( @@ -78,7 +80,7 @@ async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: except BaseException: content = f"File surfing error:\n\n{traceback.format_exc()}" - self._chat_history.append(AssistantMessage(content, source=self.name)) + self._chat_history.append(AssistantMessage(content=content, source=self.name)) return Response(chat_message=TextMessage(content=content, source=self.name)) async def on_reset(self, cancellation_token: CancellationToken) -> None: diff --git a/python/packages/autogen-ext/src/autogen_ext/agents/web_surfer/_multimodal_web_surfer.py b/python/packages/autogen-ext/src/autogen_ext/agents/web_surfer/_multimodal_web_surfer.py index 3f2e8aeaecc2..408b518de34e 100644 --- a/python/packages/autogen-ext/src/autogen_ext/agents/web_surfer/_multimodal_web_surfer.py +++ b/python/packages/autogen-ext/src/autogen_ext/agents/web_surfer/_multimodal_web_surfer.py @@ -190,7 +190,7 @@ async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: except BaseException: content = f"Web surfing error:\n\n{traceback.format_exc()}" - self._chat_history.append(AssistantMessage(content, source=self.name)) + self._chat_history.append(AssistantMessage(content=content, source=self.name)) return Response(chat_message=TextMessage(content=content, source=self.name)) async def on_reset(self, cancellation_token: CancellationToken) -> None: @@ -712,7 +712,7 @@ async def _summarize_page( for line in page_markdown.splitlines(): message = UserMessage( # content=[ - prompt + buffer + line, + content=prompt + buffer + line, # ag_image, # ], source=self.name, diff --git a/python/packages/autogen-ext/tests/models/test_reply_chat_completion_client.py b/python/packages/autogen-ext/tests/models/test_reply_chat_completion_client.py index 6f79188c7568..bd5f913bf1ad 100644 --- a/python/packages/autogen-ext/tests/models/test_reply_chat_completion_client.py +++ b/python/packages/autogen-ext/tests/models/test_reply_chat_completion_client.py @@ -33,7 +33,7 @@ async def on_new_message(self, message: ContentMessage, ctx: MessageContext) -> @property def _fixed_message_history_type(self) -> List[SystemMessage]: - return [SystemMessage(msg.content) for msg in self._chat_history] + return [SystemMessage(content=msg.content) for msg in self._chat_history] @default_subscription diff --git a/python/packages/autogen-magentic-one/src/autogen_magentic_one/agents/coder.py b/python/packages/autogen-magentic-one/src/autogen_magentic_one/agents/coder.py index 1d3e8dbd24fc..f30a8da41742 100644 --- a/python/packages/autogen-magentic-one/src/autogen_magentic_one/agents/coder.py +++ b/python/packages/autogen-magentic-one/src/autogen_magentic_one/agents/coder.py @@ -21,7 +21,8 @@ class Coder(BaseWorker): DEFAULT_DESCRIPTION = "A helpful and general-purpose AI assistant that has strong language skills, Python skills, and Linux command line skills." DEFAULT_SYSTEM_MESSAGES = [ - SystemMessage("""You are a helpful AI assistant. + SystemMessage( + content="""You are a helpful AI assistant. Solve tasks using your coding and language skills. In the following cases, suggest python code (in a python coding block) or shell script (in a sh coding block) for the user to execute. 1. When you need to collect info, use the code to output the info you need, for example, browse or search the web, download/read a file, print the content of a webpage or a file, get the current date/time, check the operating system. After sufficient info is printed and the task is ready to be solved based on your language skill, you can solve the task by yourself. @@ -31,7 +32,8 @@ class Coder(BaseWorker): If you want the user to save the code in a file before executing it, put # filename: inside the code block as the first line. Don't include multiple code blocks in one response. Do not ask users to copy and paste the result. Instead, use 'print' function for the output when relevant. Check the execution result returned by the user. If the result indicates there is an error, fix the error and output the code again. Suggest the full code instead of partial code or code changes. If the error can't be fixed or if the task is not solved even after the code is executed successfully, analyze the problem, revisit your assumption, collect additional info you need, and think of a different approach to try. When you find an answer, verify the answer carefully. Include verifiable evidence in your response if possible. -Reply "TERMINATE" in the end when everything is done.""") +Reply "TERMINATE" in the end when everything is done.""" + ) ] def __init__( diff --git a/python/packages/autogen-magentic-one/src/autogen_magentic_one/agents/file_surfer/file_surfer.py b/python/packages/autogen-magentic-one/src/autogen_magentic_one/agents/file_surfer/file_surfer.py index 3321e9c0aa00..75fba6be46e5 100644 --- a/python/packages/autogen-magentic-one/src/autogen_magentic_one/agents/file_surfer/file_surfer.py +++ b/python/packages/autogen-magentic-one/src/autogen_magentic_one/agents/file_surfer/file_surfer.py @@ -23,9 +23,11 @@ class FileSurfer(BaseWorker): DEFAULT_DESCRIPTION = "An agent that can handle local files." DEFAULT_SYSTEM_MESSAGES = [ - SystemMessage(""" + SystemMessage( + content=""" You are a helpful AI Assistant. - When given a user query, use available functions to help the user with their request."""), + When given a user query, use available functions to help the user with their request.""" + ), ] def __init__( diff --git a/python/packages/autogen-magentic-one/src/autogen_magentic_one/agents/multimodal_web_surfer/multimodal_web_surfer.py b/python/packages/autogen-magentic-one/src/autogen_magentic_one/agents/multimodal_web_surfer/multimodal_web_surfer.py index 94454b80e13b..f9a2bec6412e 100644 --- a/python/packages/autogen-magentic-one/src/autogen_magentic_one/agents/multimodal_web_surfer/multimodal_web_surfer.py +++ b/python/packages/autogen-magentic-one/src/autogen_magentic_one/agents/multimodal_web_surfer/multimodal_web_surfer.py @@ -817,7 +817,7 @@ async def _summarize_page( for line in re.split(r"([\r\n]+)", page_markdown): message = UserMessage( # content=[ - prompt + buffer + line, + content=prompt + buffer + line, # ag_image, # ], source=self.metadata["type"], diff --git a/python/packages/autogen-magentic-one/src/autogen_magentic_one/agents/orchestrator.py b/python/packages/autogen-magentic-one/src/autogen_magentic_one/agents/orchestrator.py index dedd160a2ed9..f170f45839c2 100644 --- a/python/packages/autogen-magentic-one/src/autogen_magentic_one/agents/orchestrator.py +++ b/python/packages/autogen-magentic-one/src/autogen_magentic_one/agents/orchestrator.py @@ -47,7 +47,7 @@ class LedgerOrchestrator(BaseOrchestrator): It uses a ledger (implemented as a JSON generated by the LLM) to keep track of task progress and select the next agent that should speak.""" DEFAULT_SYSTEM_MESSAGES = [ - SystemMessage(ORCHESTRATOR_SYSTEM_MESSAGE), + SystemMessage(content=ORCHESTRATOR_SYSTEM_MESSAGE), ] def __init__(