You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

test_group_chat.py 56 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341
  1. import asyncio
  2. import json
  3. import logging
  4. import tempfile
  5. from typing import Any, AsyncGenerator, List, Sequence
  6. import pytest
  7. from autogen_agentchat import EVENT_LOGGER_NAME
  8. from autogen_agentchat.agents import (
  9. AssistantAgent,
  10. BaseChatAgent,
  11. CodeExecutorAgent,
  12. )
  13. from autogen_agentchat.base import Handoff, Response, TaskResult
  14. from autogen_agentchat.conditions import HandoffTermination, MaxMessageTermination, TextMentionTermination
  15. from autogen_agentchat.messages import (
  16. AgentEvent,
  17. ChatMessage,
  18. HandoffMessage,
  19. MultiModalMessage,
  20. StopMessage,
  21. TextMessage,
  22. ToolCallExecutionEvent,
  23. ToolCallRequestEvent,
  24. ToolCallSummaryMessage,
  25. )
  26. from autogen_agentchat.teams import MagenticOneGroupChat, RoundRobinGroupChat, SelectorGroupChat, Swarm
  27. from autogen_agentchat.teams._group_chat._round_robin_group_chat import RoundRobinGroupChatManager
  28. from autogen_agentchat.teams._group_chat._selector_group_chat import SelectorGroupChatManager
  29. from autogen_agentchat.teams._group_chat._swarm_group_chat import SwarmGroupChatManager
  30. from autogen_agentchat.ui import Console
  31. from autogen_core import AgentId, CancellationToken, FunctionCall
  32. from autogen_core.models import (
  33. AssistantMessage,
  34. FunctionExecutionResult,
  35. FunctionExecutionResultMessage,
  36. LLMMessage,
  37. UserMessage,
  38. )
  39. from autogen_core.tools import FunctionTool
  40. from autogen_ext.code_executors.local import LocalCommandLineCodeExecutor
  41. from autogen_ext.models.openai import OpenAIChatCompletionClient
  42. from autogen_ext.models.replay import ReplayChatCompletionClient
  43. from openai.resources.chat.completions import AsyncCompletions
  44. from openai.types.chat.chat_completion import ChatCompletion, Choice
  45. from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
  46. from openai.types.chat.chat_completion_message import ChatCompletionMessage
  47. from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall, Function
  48. from openai.types.completion_usage import CompletionUsage
  49. from utils import FileLogHandler
  50. logger = logging.getLogger(EVENT_LOGGER_NAME)
  51. logger.setLevel(logging.DEBUG)
  52. logger.addHandler(FileLogHandler("test_group_chat.log"))
  53. class _MockChatCompletion:
  54. def __init__(self, chat_completions: List[ChatCompletion]) -> None:
  55. self._saved_chat_completions = chat_completions
  56. self._curr_index = 0
  57. async def mock_create(
  58. self, *args: Any, **kwargs: Any
  59. ) -> ChatCompletion | AsyncGenerator[ChatCompletionChunk, None]:
  60. await asyncio.sleep(0.1)
  61. completion = self._saved_chat_completions[self._curr_index]
  62. self._curr_index += 1
  63. return completion
  64. def reset(self) -> None:
  65. self._curr_index = 0
  66. class _EchoAgent(BaseChatAgent):
  67. def __init__(self, name: str, description: str) -> None:
  68. super().__init__(name, description)
  69. self._last_message: str | None = None
  70. self._total_messages = 0
  71. @property
  72. def produced_message_types(self) -> Sequence[type[ChatMessage]]:
  73. return (TextMessage,)
  74. @property
  75. def total_messages(self) -> int:
  76. return self._total_messages
  77. async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken) -> Response:
  78. if len(messages) > 0:
  79. assert isinstance(messages[0], TextMessage)
  80. self._last_message = messages[0].content
  81. self._total_messages += 1
  82. return Response(chat_message=TextMessage(content=messages[0].content, source=self.name))
  83. else:
  84. assert self._last_message is not None
  85. self._total_messages += 1
  86. return Response(chat_message=TextMessage(content=self._last_message, source=self.name))
  87. async def on_reset(self, cancellation_token: CancellationToken) -> None:
  88. self._last_message = None
  89. class _StopAgent(_EchoAgent):
  90. def __init__(self, name: str, description: str, *, stop_at: int = 1) -> None:
  91. super().__init__(name, description)
  92. self._count = 0
  93. self._stop_at = stop_at
  94. @property
  95. def produced_message_types(self) -> Sequence[type[ChatMessage]]:
  96. return (TextMessage, StopMessage)
  97. async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken) -> Response:
  98. self._count += 1
  99. if self._count < self._stop_at:
  100. return await super().on_messages(messages, cancellation_token)
  101. return Response(chat_message=StopMessage(content="TERMINATE", source=self.name))
  102. def _pass_function(input: str) -> str:
  103. return "pass"
  104. @pytest.mark.asyncio
  105. async def test_round_robin_group_chat(monkeypatch: pytest.MonkeyPatch) -> None:
  106. model = "gpt-4o-2024-05-13"
  107. chat_completions = [
  108. ChatCompletion(
  109. id="id1",
  110. choices=[
  111. Choice(
  112. finish_reason="stop",
  113. index=0,
  114. message=ChatCompletionMessage(
  115. content="""Here is the program\n ```python\nprint("Hello, world!")\n```""",
  116. role="assistant",
  117. ),
  118. )
  119. ],
  120. created=0,
  121. model=model,
  122. object="chat.completion",
  123. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  124. ),
  125. ChatCompletion(
  126. id="id2",
  127. choices=[
  128. Choice(
  129. finish_reason="stop",
  130. index=0,
  131. message=ChatCompletionMessage(content="TERMINATE", role="assistant"),
  132. )
  133. ],
  134. created=0,
  135. model=model,
  136. object="chat.completion",
  137. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  138. ),
  139. ]
  140. mock = _MockChatCompletion(chat_completions)
  141. monkeypatch.setattr(AsyncCompletions, "create", mock.mock_create)
  142. with tempfile.TemporaryDirectory() as temp_dir:
  143. code_executor_agent = CodeExecutorAgent(
  144. "code_executor", code_executor=LocalCommandLineCodeExecutor(work_dir=temp_dir)
  145. )
  146. coding_assistant_agent = AssistantAgent(
  147. "coding_assistant", model_client=OpenAIChatCompletionClient(model=model, api_key="")
  148. )
  149. termination = TextMentionTermination("TERMINATE")
  150. team = RoundRobinGroupChat(
  151. participants=[coding_assistant_agent, code_executor_agent], termination_condition=termination
  152. )
  153. result = await team.run(
  154. task="Write a program that prints 'Hello, world!'",
  155. )
  156. expected_messages = [
  157. "Write a program that prints 'Hello, world!'",
  158. 'Here is the program\n ```python\nprint("Hello, world!")\n```',
  159. "Hello, world!",
  160. "TERMINATE",
  161. ]
  162. # Normalize the messages to remove \r\n and any leading/trailing whitespace.
  163. normalized_messages = [
  164. msg.content.replace("\r\n", "\n").rstrip("\n") if isinstance(msg.content, str) else msg.content
  165. for msg in result.messages
  166. ]
  167. # Assert that all expected messages are in the collected messages
  168. assert normalized_messages == expected_messages
  169. assert result.stop_reason is not None and result.stop_reason == "Text 'TERMINATE' mentioned"
  170. # Test streaming.
  171. mock.reset()
  172. index = 0
  173. await team.reset()
  174. async for message in team.run_stream(
  175. task="Write a program that prints 'Hello, world!'",
  176. ):
  177. if isinstance(message, TaskResult):
  178. assert message == result
  179. else:
  180. assert message == result.messages[index]
  181. index += 1
  182. # Test message input.
  183. # Text message.
  184. mock.reset()
  185. index = 0
  186. await team.reset()
  187. result_2 = await team.run(
  188. task=TextMessage(content="Write a program that prints 'Hello, world!'", source="user")
  189. )
  190. assert result == result_2
  191. # Test multi-modal message.
  192. mock.reset()
  193. index = 0
  194. await team.reset()
  195. result_2 = await team.run(
  196. task=MultiModalMessage(content=["Write a program that prints 'Hello, world!'"], source="user")
  197. )
  198. assert result.messages[0].content == result_2.messages[0].content[0]
  199. assert result.messages[1:] == result_2.messages[1:]
  200. @pytest.mark.asyncio
  201. async def test_round_robin_group_chat_state() -> None:
  202. model_client = ReplayChatCompletionClient(
  203. ["No facts", "No plan", "print('Hello, world!')", "TERMINATE"],
  204. )
  205. agent1 = AssistantAgent("agent1", model_client=model_client)
  206. agent2 = AssistantAgent("agent2", model_client=model_client)
  207. termination = TextMentionTermination("TERMINATE")
  208. team1 = RoundRobinGroupChat(participants=[agent1, agent2], termination_condition=termination)
  209. await team1.run(task="Write a program that prints 'Hello, world!'")
  210. state = await team1.save_state()
  211. agent3 = AssistantAgent("agent1", model_client=model_client)
  212. agent4 = AssistantAgent("agent2", model_client=model_client)
  213. team2 = RoundRobinGroupChat(participants=[agent3, agent4], termination_condition=termination)
  214. await team2.load_state(state)
  215. state2 = await team2.save_state()
  216. assert state == state2
  217. agent1_model_ctx_messages = await agent1._model_context.get_messages() # pyright: ignore
  218. agent2_model_ctx_messages = await agent2._model_context.get_messages() # pyright: ignore
  219. agent3_model_ctx_messages = await agent3._model_context.get_messages() # pyright: ignore
  220. agent4_model_ctx_messages = await agent4._model_context.get_messages() # pyright: ignore
  221. assert agent3_model_ctx_messages == agent1_model_ctx_messages
  222. assert agent4_model_ctx_messages == agent2_model_ctx_messages
  223. manager_1 = await team1._runtime.try_get_underlying_agent_instance( # pyright: ignore
  224. AgentId("group_chat_manager", team1._team_id), # pyright: ignore
  225. RoundRobinGroupChatManager, # pyright: ignore
  226. ) # pyright: ignore
  227. manager_2 = await team2._runtime.try_get_underlying_agent_instance( # pyright: ignore
  228. AgentId("group_chat_manager", team2._team_id), # pyright: ignore
  229. RoundRobinGroupChatManager, # pyright: ignore
  230. ) # pyright: ignore
  231. assert manager_1._current_turn == manager_2._current_turn # pyright: ignore
  232. assert manager_1._message_thread == manager_2._message_thread # pyright: ignore
  233. @pytest.mark.asyncio
  234. async def test_round_robin_group_chat_with_tools(monkeypatch: pytest.MonkeyPatch) -> None:
  235. model = "gpt-4o-2024-05-13"
  236. chat_completions = [
  237. ChatCompletion(
  238. id="id1",
  239. choices=[
  240. Choice(
  241. finish_reason="tool_calls",
  242. index=0,
  243. message=ChatCompletionMessage(
  244. content=None,
  245. tool_calls=[
  246. ChatCompletionMessageToolCall(
  247. id="1",
  248. type="function",
  249. function=Function(
  250. name="pass",
  251. arguments=json.dumps({"input": "pass"}),
  252. ),
  253. )
  254. ],
  255. role="assistant",
  256. ),
  257. )
  258. ],
  259. created=0,
  260. model=model,
  261. object="chat.completion",
  262. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  263. ),
  264. ChatCompletion(
  265. id="id2",
  266. choices=[
  267. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="Hello", role="assistant"))
  268. ],
  269. created=0,
  270. model=model,
  271. object="chat.completion",
  272. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  273. ),
  274. ChatCompletion(
  275. id="id2",
  276. choices=[
  277. Choice(
  278. finish_reason="stop", index=0, message=ChatCompletionMessage(content="TERMINATE", role="assistant")
  279. )
  280. ],
  281. created=0,
  282. model=model,
  283. object="chat.completion",
  284. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  285. ),
  286. ]
  287. # Test with repeat tool calls once
  288. mock = _MockChatCompletion(chat_completions)
  289. monkeypatch.setattr(AsyncCompletions, "create", mock.mock_create)
  290. tool = FunctionTool(_pass_function, name="pass", description="pass function")
  291. tool_use_agent = AssistantAgent(
  292. "tool_use_agent",
  293. model_client=OpenAIChatCompletionClient(model=model, api_key=""),
  294. tools=[tool],
  295. )
  296. echo_agent = _EchoAgent("echo_agent", description="echo agent")
  297. termination = TextMentionTermination("TERMINATE")
  298. team = RoundRobinGroupChat(participants=[tool_use_agent, echo_agent], termination_condition=termination)
  299. result = await team.run(
  300. task="Write a program that prints 'Hello, world!'",
  301. )
  302. assert len(result.messages) == 8
  303. assert isinstance(result.messages[0], TextMessage) # task
  304. assert isinstance(result.messages[1], ToolCallRequestEvent) # tool call
  305. assert isinstance(result.messages[2], ToolCallExecutionEvent) # tool call result
  306. assert isinstance(result.messages[3], ToolCallSummaryMessage) # tool use agent response
  307. assert result.messages[3].content == "pass" # ensure the tool call was executed
  308. assert isinstance(result.messages[4], TextMessage) # echo agent response
  309. assert isinstance(result.messages[5], TextMessage) # tool use agent response
  310. assert isinstance(result.messages[6], TextMessage) # echo agent response
  311. assert isinstance(result.messages[7], TextMessage) # tool use agent response, that has TERMINATE
  312. assert result.messages[7].content == "TERMINATE"
  313. assert result.stop_reason is not None and result.stop_reason == "Text 'TERMINATE' mentioned"
  314. # Test streaming.
  315. await tool_use_agent._model_context.clear() # pyright: ignore
  316. mock.reset()
  317. index = 0
  318. await team.reset()
  319. async for message in team.run_stream(
  320. task="Write a program that prints 'Hello, world!'",
  321. ):
  322. if isinstance(message, TaskResult):
  323. assert message == result
  324. else:
  325. assert message == result.messages[index]
  326. index += 1
  327. # Test Console.
  328. await tool_use_agent._model_context.clear() # pyright: ignore
  329. mock.reset()
  330. index = 0
  331. await team.reset()
  332. result2 = await Console(team.run_stream(task="Write a program that prints 'Hello, world!'"))
  333. assert result2 == result
  334. @pytest.mark.asyncio
  335. async def test_round_robin_group_chat_with_resume_and_reset() -> None:
  336. agent_1 = _EchoAgent("agent_1", description="echo agent 1")
  337. agent_2 = _EchoAgent("agent_2", description="echo agent 2")
  338. agent_3 = _EchoAgent("agent_3", description="echo agent 3")
  339. agent_4 = _EchoAgent("agent_4", description="echo agent 4")
  340. termination = MaxMessageTermination(3)
  341. team = RoundRobinGroupChat(participants=[agent_1, agent_2, agent_3, agent_4], termination_condition=termination)
  342. result = await team.run(
  343. task="Write a program that prints 'Hello, world!'",
  344. )
  345. assert len(result.messages) == 3
  346. assert result.messages[1].source == "agent_1"
  347. assert result.messages[2].source == "agent_2"
  348. assert result.stop_reason is not None
  349. # Resume.
  350. result = await team.run()
  351. assert len(result.messages) == 3
  352. assert result.messages[0].source == "agent_3"
  353. assert result.messages[1].source == "agent_4"
  354. assert result.messages[2].source == "agent_1"
  355. assert result.stop_reason is not None
  356. # Reset.
  357. await team.reset()
  358. result = await team.run(task="Write a program that prints 'Hello, world!'")
  359. assert len(result.messages) == 3
  360. assert result.messages[1].source == "agent_1"
  361. assert result.messages[2].source == "agent_2"
  362. assert result.stop_reason is not None
  363. @pytest.mark.asyncio
  364. async def test_round_robin_group_chat_max_turn() -> None:
  365. agent_1 = _EchoAgent("agent_1", description="echo agent 1")
  366. agent_2 = _EchoAgent("agent_2", description="echo agent 2")
  367. agent_3 = _EchoAgent("agent_3", description="echo agent 3")
  368. agent_4 = _EchoAgent("agent_4", description="echo agent 4")
  369. team = RoundRobinGroupChat(participants=[agent_1, agent_2, agent_3, agent_4], max_turns=3)
  370. result = await team.run(
  371. task="Write a program that prints 'Hello, world!'",
  372. )
  373. assert len(result.messages) == 4
  374. assert result.messages[1].source == "agent_1"
  375. assert result.messages[2].source == "agent_2"
  376. assert result.messages[3].source == "agent_3"
  377. assert result.stop_reason is not None
  378. # Resume.
  379. result = await team.run()
  380. assert len(result.messages) == 3
  381. assert result.messages[0].source == "agent_4"
  382. assert result.messages[1].source == "agent_1"
  383. assert result.messages[2].source == "agent_2"
  384. assert result.stop_reason is not None
  385. # Reset.
  386. await team.reset()
  387. result = await team.run(task="Write a program that prints 'Hello, world!'")
  388. assert len(result.messages) == 4
  389. assert result.messages[1].source == "agent_1"
  390. assert result.messages[2].source == "agent_2"
  391. assert result.messages[3].source == "agent_3"
  392. assert result.stop_reason is not None
  393. @pytest.mark.asyncio
  394. async def test_round_robin_group_chat_cancellation() -> None:
  395. agent_1 = _EchoAgent("agent_1", description="echo agent 1")
  396. agent_2 = _EchoAgent("agent_2", description="echo agent 2")
  397. agent_3 = _EchoAgent("agent_3", description="echo agent 3")
  398. agent_4 = _EchoAgent("agent_4", description="echo agent 4")
  399. # Set max_turns to a large number to avoid stopping due to max_turns before cancellation.
  400. team = RoundRobinGroupChat(participants=[agent_1, agent_2, agent_3, agent_4], max_turns=1000)
  401. cancellation_token = CancellationToken()
  402. run_task = asyncio.create_task(
  403. team.run(
  404. task="Write a program that prints 'Hello, world!'",
  405. cancellation_token=cancellation_token,
  406. )
  407. )
  408. await asyncio.sleep(0.1)
  409. # Cancel the task.
  410. cancellation_token.cancel()
  411. with pytest.raises(asyncio.CancelledError):
  412. await run_task
  413. # Total messages produced so far.
  414. total_messages = agent_1.total_messages + agent_2.total_messages + agent_3.total_messages + agent_4.total_messages
  415. # Still can run again and finish the task.
  416. result = await team.run()
  417. assert len(result.messages) + total_messages == 1000
  418. @pytest.mark.asyncio
  419. async def test_selector_group_chat(monkeypatch: pytest.MonkeyPatch) -> None:
  420. model = "gpt-4o-2024-05-13"
  421. chat_completions = [
  422. ChatCompletion(
  423. id="id2",
  424. choices=[
  425. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="agent3", role="assistant"))
  426. ],
  427. created=0,
  428. model=model,
  429. object="chat.completion",
  430. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  431. ),
  432. ChatCompletion(
  433. id="id2",
  434. choices=[
  435. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="agent2", role="assistant"))
  436. ],
  437. created=0,
  438. model=model,
  439. object="chat.completion",
  440. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  441. ),
  442. ChatCompletion(
  443. id="id2",
  444. choices=[
  445. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="agent1", role="assistant"))
  446. ],
  447. created=0,
  448. model=model,
  449. object="chat.completion",
  450. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  451. ),
  452. ChatCompletion(
  453. id="id2",
  454. choices=[
  455. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="agent2", role="assistant"))
  456. ],
  457. created=0,
  458. model=model,
  459. object="chat.completion",
  460. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  461. ),
  462. ChatCompletion(
  463. id="id2",
  464. choices=[
  465. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="agent1", role="assistant"))
  466. ],
  467. created=0,
  468. model=model,
  469. object="chat.completion",
  470. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  471. ),
  472. ]
  473. mock = _MockChatCompletion(chat_completions)
  474. monkeypatch.setattr(AsyncCompletions, "create", mock.mock_create)
  475. agent1 = _StopAgent("agent1", description="echo agent 1", stop_at=2)
  476. agent2 = _EchoAgent("agent2", description="echo agent 2")
  477. agent3 = _EchoAgent("agent3", description="echo agent 3")
  478. termination = TextMentionTermination("TERMINATE")
  479. team = SelectorGroupChat(
  480. participants=[agent1, agent2, agent3],
  481. model_client=OpenAIChatCompletionClient(model=model, api_key=""),
  482. termination_condition=termination,
  483. )
  484. result = await team.run(
  485. task="Write a program that prints 'Hello, world!'",
  486. )
  487. assert len(result.messages) == 6
  488. assert result.messages[0].content == "Write a program that prints 'Hello, world!'"
  489. assert result.messages[1].source == "agent3"
  490. assert result.messages[2].source == "agent2"
  491. assert result.messages[3].source == "agent1"
  492. assert result.messages[4].source == "agent2"
  493. assert result.messages[5].source == "agent1"
  494. assert result.stop_reason is not None and result.stop_reason == "Text 'TERMINATE' mentioned"
  495. # Test streaming.
  496. mock.reset()
  497. agent1._count = 0 # pyright: ignore
  498. index = 0
  499. await team.reset()
  500. async for message in team.run_stream(
  501. task="Write a program that prints 'Hello, world!'",
  502. ):
  503. if isinstance(message, TaskResult):
  504. assert message == result
  505. else:
  506. assert message == result.messages[index]
  507. index += 1
  508. # Test Console.
  509. mock.reset()
  510. agent1._count = 0 # pyright: ignore
  511. index = 0
  512. await team.reset()
  513. result2 = await Console(team.run_stream(task="Write a program that prints 'Hello, world!'"))
  514. assert result2 == result
  515. @pytest.mark.asyncio
  516. async def test_selector_group_chat_state() -> None:
  517. model_client = ReplayChatCompletionClient(
  518. ["agent1", "No facts", "agent2", "No plan", "agent1", "print('Hello, world!')", "agent2", "TERMINATE"],
  519. )
  520. agent1 = AssistantAgent("agent1", model_client=model_client)
  521. agent2 = AssistantAgent("agent2", model_client=model_client)
  522. termination = TextMentionTermination("TERMINATE")
  523. team1 = SelectorGroupChat(
  524. participants=[agent1, agent2], termination_condition=termination, model_client=model_client
  525. )
  526. await team1.run(task="Write a program that prints 'Hello, world!'")
  527. state = await team1.save_state()
  528. agent3 = AssistantAgent("agent1", model_client=model_client)
  529. agent4 = AssistantAgent("agent2", model_client=model_client)
  530. team2 = SelectorGroupChat(
  531. participants=[agent3, agent4], termination_condition=termination, model_client=model_client
  532. )
  533. await team2.load_state(state)
  534. state2 = await team2.save_state()
  535. assert state == state2
  536. agent1_model_ctx_messages = await agent1._model_context.get_messages() # pyright: ignore
  537. agent2_model_ctx_messages = await agent2._model_context.get_messages() # pyright: ignore
  538. agent3_model_ctx_messages = await agent3._model_context.get_messages() # pyright: ignore
  539. agent4_model_ctx_messages = await agent4._model_context.get_messages() # pyright: ignore
  540. assert agent3_model_ctx_messages == agent1_model_ctx_messages
  541. assert agent4_model_ctx_messages == agent2_model_ctx_messages
  542. manager_1 = await team1._runtime.try_get_underlying_agent_instance( # pyright: ignore
  543. AgentId("group_chat_manager", team1._team_id), # pyright: ignore
  544. SelectorGroupChatManager, # pyright: ignore
  545. ) # pyright: ignore
  546. manager_2 = await team2._runtime.try_get_underlying_agent_instance( # pyright: ignore
  547. AgentId("group_chat_manager", team2._team_id), # pyright: ignore
  548. SelectorGroupChatManager, # pyright: ignore
  549. ) # pyright: ignore
  550. assert manager_1._message_thread == manager_2._message_thread # pyright: ignore
  551. assert manager_1._previous_speaker == manager_2._previous_speaker # pyright: ignore
  552. @pytest.mark.asyncio
  553. async def test_selector_group_chat_two_speakers(monkeypatch: pytest.MonkeyPatch) -> None:
  554. model = "gpt-4o-2024-05-13"
  555. chat_completions = [
  556. ChatCompletion(
  557. id="id2",
  558. choices=[
  559. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="agent2", role="assistant"))
  560. ],
  561. created=0,
  562. model=model,
  563. object="chat.completion",
  564. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  565. ),
  566. ]
  567. mock = _MockChatCompletion(chat_completions)
  568. monkeypatch.setattr(AsyncCompletions, "create", mock.mock_create)
  569. agent1 = _StopAgent("agent1", description="echo agent 1", stop_at=2)
  570. agent2 = _EchoAgent("agent2", description="echo agent 2")
  571. termination = TextMentionTermination("TERMINATE")
  572. team = SelectorGroupChat(
  573. participants=[agent1, agent2],
  574. termination_condition=termination,
  575. model_client=OpenAIChatCompletionClient(model=model, api_key=""),
  576. )
  577. result = await team.run(
  578. task="Write a program that prints 'Hello, world!'",
  579. )
  580. assert len(result.messages) == 5
  581. assert result.messages[0].content == "Write a program that prints 'Hello, world!'"
  582. assert result.messages[1].source == "agent2"
  583. assert result.messages[2].source == "agent1"
  584. assert result.messages[3].source == "agent2"
  585. assert result.messages[4].source == "agent1"
  586. # only one chat completion was called
  587. assert mock._curr_index == 1 # pyright: ignore
  588. assert result.stop_reason is not None and result.stop_reason == "Text 'TERMINATE' mentioned"
  589. # Test streaming.
  590. mock.reset()
  591. agent1._count = 0 # pyright: ignore
  592. index = 0
  593. await team.reset()
  594. async for message in team.run_stream(task="Write a program that prints 'Hello, world!'"):
  595. if isinstance(message, TaskResult):
  596. assert message == result
  597. else:
  598. assert message == result.messages[index]
  599. index += 1
  600. # Test Console.
  601. mock.reset()
  602. agent1._count = 0 # pyright: ignore
  603. index = 0
  604. await team.reset()
  605. result2 = await Console(team.run_stream(task="Write a program that prints 'Hello, world!'"))
  606. assert result2 == result
  607. @pytest.mark.asyncio
  608. async def test_selector_group_chat_two_speakers_allow_repeated(monkeypatch: pytest.MonkeyPatch) -> None:
  609. model = "gpt-4o-2024-05-13"
  610. chat_completions = [
  611. ChatCompletion(
  612. id="id2",
  613. choices=[
  614. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="agent2", role="assistant"))
  615. ],
  616. created=0,
  617. model=model,
  618. object="chat.completion",
  619. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  620. ),
  621. ChatCompletion(
  622. id="id2",
  623. choices=[
  624. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="agent2", role="assistant"))
  625. ],
  626. created=0,
  627. model=model,
  628. object="chat.completion",
  629. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  630. ),
  631. ChatCompletion(
  632. id="id2",
  633. choices=[
  634. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="agent1", role="assistant"))
  635. ],
  636. created=0,
  637. model=model,
  638. object="chat.completion",
  639. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  640. ),
  641. ]
  642. mock = _MockChatCompletion(chat_completions)
  643. monkeypatch.setattr(AsyncCompletions, "create", mock.mock_create)
  644. agent1 = _StopAgent("agent1", description="echo agent 1", stop_at=1)
  645. agent2 = _EchoAgent("agent2", description="echo agent 2")
  646. termination = TextMentionTermination("TERMINATE")
  647. team = SelectorGroupChat(
  648. participants=[agent1, agent2],
  649. model_client=OpenAIChatCompletionClient(model=model, api_key=""),
  650. termination_condition=termination,
  651. allow_repeated_speaker=True,
  652. )
  653. result = await team.run(task="Write a program that prints 'Hello, world!'")
  654. assert len(result.messages) == 4
  655. assert result.messages[0].content == "Write a program that prints 'Hello, world!'"
  656. assert result.messages[1].source == "agent2"
  657. assert result.messages[2].source == "agent2"
  658. assert result.messages[3].source == "agent1"
  659. assert result.stop_reason is not None and result.stop_reason == "Text 'TERMINATE' mentioned"
  660. # Test streaming.
  661. mock.reset()
  662. index = 0
  663. await team.reset()
  664. async for message in team.run_stream(task="Write a program that prints 'Hello, world!'"):
  665. if isinstance(message, TaskResult):
  666. assert message == result
  667. else:
  668. assert message == result.messages[index]
  669. index += 1
  670. # Test Console.
  671. mock.reset()
  672. index = 0
  673. await team.reset()
  674. result2 = await Console(team.run_stream(task="Write a program that prints 'Hello, world!'"))
  675. assert result2 == result
  676. @pytest.mark.asyncio
  677. async def test_selector_group_chat_succcess_after_2_attempts() -> None:
  678. model_client = ReplayChatCompletionClient(
  679. ["agent2, agent3", "agent2"],
  680. )
  681. agent1 = _StopAgent("agent1", description="echo agent 1", stop_at=1)
  682. agent2 = _EchoAgent("agent2", description="echo agent 2")
  683. agent3 = _EchoAgent("agent3", description="echo agent 3")
  684. team = SelectorGroupChat(
  685. participants=[agent1, agent2, agent3],
  686. model_client=model_client,
  687. max_turns=1,
  688. )
  689. result = await team.run(task="Write a program that prints 'Hello, world!'")
  690. assert len(result.messages) == 2
  691. assert result.messages[0].content == "Write a program that prints 'Hello, world!'"
  692. assert result.messages[1].source == "agent2"
  693. @pytest.mark.asyncio
  694. async def test_selector_group_chat_fall_back_to_first_after_3_attempts() -> None:
  695. model_client = ReplayChatCompletionClient(
  696. [
  697. "agent2, agent3", # Multiple speakers
  698. "agent5", # Non-existent speaker
  699. "agent3, agent1", # Multiple speakers
  700. ]
  701. )
  702. agent1 = _StopAgent("agent1", description="echo agent 1", stop_at=1)
  703. agent2 = _EchoAgent("agent2", description="echo agent 2")
  704. agent3 = _EchoAgent("agent3", description="echo agent 3")
  705. team = SelectorGroupChat(
  706. participants=[agent1, agent2, agent3],
  707. model_client=model_client,
  708. max_turns=1,
  709. )
  710. result = await team.run(task="Write a program that prints 'Hello, world!'")
  711. assert len(result.messages) == 2
  712. assert result.messages[0].content == "Write a program that prints 'Hello, world!'"
  713. assert result.messages[1].source == "agent1"
  714. @pytest.mark.asyncio
  715. async def test_selector_group_chat_fall_back_to_previous_after_3_attempts() -> None:
  716. model_client = ReplayChatCompletionClient(
  717. ["agent2", "agent2", "agent2", "agent2"],
  718. )
  719. agent1 = _StopAgent("agent1", description="echo agent 1", stop_at=1)
  720. agent2 = _EchoAgent("agent2", description="echo agent 2")
  721. agent3 = _EchoAgent("agent3", description="echo agent 3")
  722. team = SelectorGroupChat(
  723. participants=[agent1, agent2, agent3],
  724. model_client=model_client,
  725. max_turns=2,
  726. )
  727. result = await team.run(task="Write a program that prints 'Hello, world!'")
  728. assert len(result.messages) == 3
  729. assert result.messages[0].content == "Write a program that prints 'Hello, world!'"
  730. assert result.messages[1].source == "agent2"
  731. assert result.messages[2].source == "agent2"
  732. @pytest.mark.asyncio
  733. async def test_selector_group_chat_custom_selector(monkeypatch: pytest.MonkeyPatch) -> None:
  734. model = "gpt-4o-2024-05-13"
  735. chat_completions = [
  736. ChatCompletion(
  737. id="id2",
  738. choices=[
  739. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="agent3", role="assistant"))
  740. ],
  741. created=0,
  742. model=model,
  743. object="chat.completion",
  744. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  745. ),
  746. ]
  747. mock = _MockChatCompletion(chat_completions)
  748. monkeypatch.setattr(AsyncCompletions, "create", mock.mock_create)
  749. agent1 = _EchoAgent("agent1", description="echo agent 1")
  750. agent2 = _EchoAgent("agent2", description="echo agent 2")
  751. agent3 = _EchoAgent("agent3", description="echo agent 3")
  752. agent4 = _EchoAgent("agent4", description="echo agent 4")
  753. def _select_agent(messages: Sequence[AgentEvent | ChatMessage]) -> str | None:
  754. if len(messages) == 0:
  755. return "agent1"
  756. elif messages[-1].source == "agent1":
  757. return "agent2"
  758. elif messages[-1].source == "agent2":
  759. return None
  760. elif messages[-1].source == "agent3":
  761. return "agent4"
  762. else:
  763. return "agent1"
  764. termination = MaxMessageTermination(6)
  765. team = SelectorGroupChat(
  766. participants=[agent1, agent2, agent3, agent4],
  767. model_client=OpenAIChatCompletionClient(model=model, api_key=""),
  768. selector_func=_select_agent,
  769. termination_condition=termination,
  770. )
  771. result = await team.run(task="task")
  772. assert len(result.messages) == 6
  773. assert result.messages[1].source == "agent1"
  774. assert result.messages[2].source == "agent2"
  775. assert result.messages[3].source == "agent3"
  776. assert result.messages[4].source == "agent4"
  777. assert result.messages[5].source == "agent1"
  778. assert (
  779. result.stop_reason is not None
  780. and result.stop_reason == "Maximum number of messages 6 reached, current message count: 6"
  781. )
  782. class _HandOffAgent(BaseChatAgent):
  783. def __init__(self, name: str, description: str, next_agent: str) -> None:
  784. super().__init__(name, description)
  785. self._next_agent = next_agent
  786. @property
  787. def produced_message_types(self) -> Sequence[type[ChatMessage]]:
  788. return (HandoffMessage,)
  789. async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken) -> Response:
  790. return Response(
  791. chat_message=HandoffMessage(
  792. content=f"Transferred to {self._next_agent}.", target=self._next_agent, source=self.name
  793. )
  794. )
  795. async def on_reset(self, cancellation_token: CancellationToken) -> None:
  796. pass
  797. @pytest.mark.asyncio
  798. async def test_swarm_handoff() -> None:
  799. first_agent = _HandOffAgent("first_agent", description="first agent", next_agent="second_agent")
  800. second_agent = _HandOffAgent("second_agent", description="second agent", next_agent="third_agent")
  801. third_agent = _HandOffAgent("third_agent", description="third agent", next_agent="first_agent")
  802. termination = MaxMessageTermination(6)
  803. team = Swarm([second_agent, first_agent, third_agent], termination_condition=termination)
  804. result = await team.run(task="task")
  805. assert len(result.messages) == 6
  806. assert result.messages[0].content == "task"
  807. assert result.messages[1].content == "Transferred to third_agent."
  808. assert result.messages[2].content == "Transferred to first_agent."
  809. assert result.messages[3].content == "Transferred to second_agent."
  810. assert result.messages[4].content == "Transferred to third_agent."
  811. assert result.messages[5].content == "Transferred to first_agent."
  812. assert (
  813. result.stop_reason is not None
  814. and result.stop_reason == "Maximum number of messages 6 reached, current message count: 6"
  815. )
  816. # Test streaming.
  817. index = 0
  818. await team.reset()
  819. stream = team.run_stream(task="task")
  820. async for message in stream:
  821. if isinstance(message, TaskResult):
  822. assert message == result
  823. else:
  824. assert message == result.messages[index]
  825. index += 1
  826. # Test save and load.
  827. state = await team.save_state()
  828. first_agent2 = _HandOffAgent("first_agent", description="first agent", next_agent="second_agent")
  829. second_agent2 = _HandOffAgent("second_agent", description="second agent", next_agent="third_agent")
  830. third_agent2 = _HandOffAgent("third_agent", description="third agent", next_agent="first_agent")
  831. team2 = Swarm([second_agent2, first_agent2, third_agent2], termination_condition=termination)
  832. await team2.load_state(state)
  833. state2 = await team2.save_state()
  834. assert state == state2
  835. manager_1 = await team._runtime.try_get_underlying_agent_instance( # pyright: ignore
  836. AgentId("group_chat_manager", team._team_id), # pyright: ignore
  837. SwarmGroupChatManager, # pyright: ignore
  838. ) # pyright: ignore
  839. manager_2 = await team2._runtime.try_get_underlying_agent_instance( # pyright: ignore
  840. AgentId("group_chat_manager", team2._team_id), # pyright: ignore
  841. SwarmGroupChatManager, # pyright: ignore
  842. ) # pyright: ignore
  843. assert manager_1._message_thread == manager_2._message_thread # pyright: ignore
  844. assert manager_1._current_speaker == manager_2._current_speaker # pyright: ignore
  845. @pytest.mark.asyncio
  846. async def test_swarm_handoff_using_tool_calls(monkeypatch: pytest.MonkeyPatch) -> None:
  847. model = "gpt-4o-2024-05-13"
  848. chat_completions = [
  849. ChatCompletion(
  850. id="id1",
  851. choices=[
  852. Choice(
  853. finish_reason="tool_calls",
  854. index=0,
  855. message=ChatCompletionMessage(
  856. content=None,
  857. tool_calls=[
  858. ChatCompletionMessageToolCall(
  859. id="1",
  860. type="function",
  861. function=Function(
  862. name="handoff_to_agent2",
  863. arguments=json.dumps({}),
  864. ),
  865. )
  866. ],
  867. role="assistant",
  868. ),
  869. )
  870. ],
  871. created=0,
  872. model=model,
  873. object="chat.completion",
  874. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  875. ),
  876. ChatCompletion(
  877. id="id2",
  878. choices=[
  879. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="Hello", role="assistant"))
  880. ],
  881. created=0,
  882. model=model,
  883. object="chat.completion",
  884. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  885. ),
  886. ChatCompletion(
  887. id="id2",
  888. choices=[
  889. Choice(
  890. finish_reason="stop", index=0, message=ChatCompletionMessage(content="TERMINATE", role="assistant")
  891. )
  892. ],
  893. created=0,
  894. model=model,
  895. object="chat.completion",
  896. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  897. ),
  898. ]
  899. mock = _MockChatCompletion(chat_completions)
  900. monkeypatch.setattr(AsyncCompletions, "create", mock.mock_create)
  901. agent1 = AssistantAgent(
  902. "agent1",
  903. model_client=OpenAIChatCompletionClient(model=model, api_key=""),
  904. handoffs=[Handoff(target="agent2", name="handoff_to_agent2", message="handoff to agent2")],
  905. )
  906. agent2 = _HandOffAgent("agent2", description="agent 2", next_agent="agent1")
  907. termination = TextMentionTermination("TERMINATE")
  908. team = Swarm([agent1, agent2], termination_condition=termination)
  909. result = await team.run(task="task")
  910. assert len(result.messages) == 7
  911. assert result.messages[0].content == "task"
  912. assert isinstance(result.messages[1], ToolCallRequestEvent)
  913. assert isinstance(result.messages[2], ToolCallExecutionEvent)
  914. assert result.messages[3].content == "handoff to agent2"
  915. assert result.messages[4].content == "Transferred to agent1."
  916. assert result.messages[5].content == "Hello"
  917. assert result.messages[6].content == "TERMINATE"
  918. assert result.stop_reason is not None and result.stop_reason == "Text 'TERMINATE' mentioned"
  919. # Test streaming.
  920. await agent1._model_context.clear() # pyright: ignore
  921. mock.reset()
  922. index = 0
  923. await team.reset()
  924. stream = team.run_stream(task="task")
  925. async for message in stream:
  926. if isinstance(message, TaskResult):
  927. assert message == result
  928. else:
  929. assert message == result.messages[index]
  930. index += 1
  931. # Test Console
  932. await agent1._model_context.clear() # pyright: ignore
  933. mock.reset()
  934. index = 0
  935. await team.reset()
  936. result2 = await Console(team.run_stream(task="task"))
  937. assert result2 == result
  938. @pytest.mark.asyncio
  939. async def test_swarm_pause_and_resume() -> None:
  940. first_agent = _HandOffAgent("first_agent", description="first agent", next_agent="second_agent")
  941. second_agent = _HandOffAgent("second_agent", description="second agent", next_agent="third_agent")
  942. third_agent = _HandOffAgent("third_agent", description="third agent", next_agent="first_agent")
  943. team = Swarm([second_agent, first_agent, third_agent], max_turns=1)
  944. result = await team.run(task="task")
  945. assert len(result.messages) == 2
  946. assert result.messages[0].content == "task"
  947. assert result.messages[1].content == "Transferred to third_agent."
  948. # Resume with a new task.
  949. result = await team.run(task="new task")
  950. assert len(result.messages) == 2
  951. assert result.messages[0].content == "new task"
  952. assert result.messages[1].content == "Transferred to first_agent."
  953. # Resume with the same task.
  954. result = await team.run()
  955. assert len(result.messages) == 1
  956. assert result.messages[0].content == "Transferred to second_agent."
  957. @pytest.mark.asyncio
  958. async def test_swarm_with_parallel_tool_calls(monkeypatch: pytest.MonkeyPatch) -> None:
  959. model = "gpt-4o-2024-05-13"
  960. chat_completions = [
  961. ChatCompletion(
  962. id="id1",
  963. choices=[
  964. Choice(
  965. finish_reason="tool_calls",
  966. index=0,
  967. message=ChatCompletionMessage(
  968. content=None,
  969. tool_calls=[
  970. ChatCompletionMessageToolCall(
  971. id="1",
  972. type="function",
  973. function=Function(
  974. name="tool1",
  975. arguments=json.dumps({}),
  976. ),
  977. ),
  978. ChatCompletionMessageToolCall(
  979. id="2",
  980. type="function",
  981. function=Function(
  982. name="tool2",
  983. arguments=json.dumps({}),
  984. ),
  985. ),
  986. ChatCompletionMessageToolCall(
  987. id="3",
  988. type="function",
  989. function=Function(
  990. name="handoff_to_agent2",
  991. arguments=json.dumps({}),
  992. ),
  993. ),
  994. ],
  995. role="assistant",
  996. ),
  997. )
  998. ],
  999. created=0,
  1000. model=model,
  1001. object="chat.completion",
  1002. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  1003. ),
  1004. ChatCompletion(
  1005. id="id2",
  1006. choices=[
  1007. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="Hello", role="assistant"))
  1008. ],
  1009. created=0,
  1010. model=model,
  1011. object="chat.completion",
  1012. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  1013. ),
  1014. ChatCompletion(
  1015. id="id2",
  1016. choices=[
  1017. Choice(
  1018. finish_reason="stop", index=0, message=ChatCompletionMessage(content="TERMINATE", role="assistant")
  1019. )
  1020. ],
  1021. created=0,
  1022. model=model,
  1023. object="chat.completion",
  1024. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  1025. ),
  1026. ]
  1027. mock = _MockChatCompletion(chat_completions)
  1028. monkeypatch.setattr(AsyncCompletions, "create", mock.mock_create)
  1029. expected_handoff_context: List[LLMMessage] = [
  1030. AssistantMessage(
  1031. source="agent1",
  1032. content=[
  1033. FunctionCall(id="1", name="tool1", arguments="{}"),
  1034. FunctionCall(id="2", name="tool2", arguments="{}"),
  1035. ],
  1036. ),
  1037. FunctionExecutionResultMessage(
  1038. content=[
  1039. FunctionExecutionResult(content="tool1", call_id="1", is_error=False),
  1040. FunctionExecutionResult(content="tool2", call_id="2", is_error=False),
  1041. ]
  1042. ),
  1043. ]
  1044. def tool1() -> str:
  1045. return "tool1"
  1046. def tool2() -> str:
  1047. return "tool2"
  1048. agent1 = AssistantAgent(
  1049. "agent1",
  1050. model_client=OpenAIChatCompletionClient(model=model, api_key=""),
  1051. handoffs=[Handoff(target="agent2", name="handoff_to_agent2", message="handoff to agent2")],
  1052. tools=[tool1, tool2],
  1053. )
  1054. agent2 = AssistantAgent(
  1055. "agent2",
  1056. model_client=OpenAIChatCompletionClient(model=model, api_key=""),
  1057. )
  1058. termination = TextMentionTermination("TERMINATE")
  1059. team = Swarm([agent1, agent2], termination_condition=termination)
  1060. result = await team.run(task="task")
  1061. assert len(result.messages) == 6
  1062. assert result.messages[0] == TextMessage(content="task", source="user")
  1063. assert isinstance(result.messages[1], ToolCallRequestEvent)
  1064. assert isinstance(result.messages[2], ToolCallExecutionEvent)
  1065. assert result.messages[3] == HandoffMessage(
  1066. content="handoff to agent2",
  1067. target="agent2",
  1068. source="agent1",
  1069. context=expected_handoff_context,
  1070. )
  1071. assert result.messages[4].content == "Hello"
  1072. assert result.messages[4].source == "agent2"
  1073. assert result.messages[5].content == "TERMINATE"
  1074. assert result.messages[5].source == "agent2"
  1075. # Verify the tool calls are in agent2's context.
  1076. agent2_model_ctx_messages = await agent2._model_context.get_messages() # pyright: ignore
  1077. assert agent2_model_ctx_messages[0] == UserMessage(content="task", source="user")
  1078. assert agent2_model_ctx_messages[1] == expected_handoff_context[0]
  1079. assert agent2_model_ctx_messages[2] == expected_handoff_context[1]
  1080. @pytest.mark.asyncio
  1081. async def test_swarm_with_handoff_termination() -> None:
  1082. first_agent = _HandOffAgent("first_agent", description="first agent", next_agent="second_agent")
  1083. second_agent = _HandOffAgent("second_agent", description="second agent", next_agent="third_agent")
  1084. third_agent = _HandOffAgent("third_agent", description="third agent", next_agent="first_agent")
  1085. # Handoff to an existing agent.
  1086. termination = HandoffTermination(target="third_agent")
  1087. team = Swarm([second_agent, first_agent, third_agent], termination_condition=termination)
  1088. # Start
  1089. result = await team.run(task="task")
  1090. assert len(result.messages) == 2
  1091. assert result.messages[0].content == "task"
  1092. assert result.messages[1].content == "Transferred to third_agent."
  1093. # Resume existing.
  1094. result = await team.run()
  1095. assert len(result.messages) == 3
  1096. assert result.messages[0].content == "Transferred to first_agent."
  1097. assert result.messages[1].content == "Transferred to second_agent."
  1098. assert result.messages[2].content == "Transferred to third_agent."
  1099. # Resume new task.
  1100. result = await team.run(task="new task")
  1101. assert len(result.messages) == 4
  1102. assert result.messages[0].content == "new task"
  1103. assert result.messages[1].content == "Transferred to first_agent."
  1104. assert result.messages[2].content == "Transferred to second_agent."
  1105. assert result.messages[3].content == "Transferred to third_agent."
  1106. # Handoff to a non-existing agent.
  1107. third_agent = _HandOffAgent("third_agent", description="third agent", next_agent="non_existing_agent")
  1108. termination = HandoffTermination(target="non_existing_agent")
  1109. team = Swarm([second_agent, first_agent, third_agent], termination_condition=termination)
  1110. # Start
  1111. result = await team.run(task="task")
  1112. assert len(result.messages) == 3
  1113. assert result.messages[0].content == "task"
  1114. assert result.messages[1].content == "Transferred to third_agent."
  1115. assert result.messages[2].content == "Transferred to non_existing_agent."
  1116. # Attempt to resume.
  1117. with pytest.raises(ValueError):
  1118. await team.run()
  1119. # Attempt to resume with a new task.
  1120. with pytest.raises(ValueError):
  1121. await team.run(task="new task")
  1122. # Resume with a HandoffMessage
  1123. result = await team.run(task=HandoffMessage(content="Handoff to first_agent.", target="first_agent", source="user"))
  1124. assert len(result.messages) == 4
  1125. assert result.messages[0].content == "Handoff to first_agent."
  1126. assert result.messages[1].content == "Transferred to second_agent."
  1127. assert result.messages[2].content == "Transferred to third_agent."
  1128. assert result.messages[3].content == "Transferred to non_existing_agent."
  1129. @pytest.mark.asyncio
  1130. async def test_round_robin_group_chat_with_message_list() -> None:
  1131. # Create a simple team with echo agents
  1132. agent1 = _EchoAgent("Agent1", "First agent")
  1133. agent2 = _EchoAgent("Agent2", "Second agent")
  1134. termination = MaxMessageTermination(4) # Stop after 4 messages
  1135. team = RoundRobinGroupChat([agent1, agent2], termination_condition=termination)
  1136. # Create a list of messages
  1137. messages: List[ChatMessage] = [
  1138. TextMessage(content="Message 1", source="user"),
  1139. TextMessage(content="Message 2", source="user"),
  1140. TextMessage(content="Message 3", source="user"),
  1141. ]
  1142. # Run the team with the message list
  1143. result = await team.run(task=messages)
  1144. # Verify the messages were processed in order
  1145. assert len(result.messages) == 4 # Initial messages + echo until termination
  1146. assert result.messages[0].content == "Message 1" # First message
  1147. assert result.messages[1].content == "Message 2" # Second message
  1148. assert result.messages[2].content == "Message 3" # Third message
  1149. assert result.messages[3].content == "Message 1" # Echo from first agent
  1150. assert result.stop_reason == "Maximum number of messages 4 reached, current message count: 4"
  1151. # Test with streaming
  1152. await team.reset()
  1153. index = 0
  1154. async for message in team.run_stream(task=messages):
  1155. if isinstance(message, TaskResult):
  1156. assert message == result
  1157. else:
  1158. assert message == result.messages[index]
  1159. index += 1
  1160. # Test with invalid message list
  1161. with pytest.raises(ValueError, match="All messages in task list must be valid ChatMessage types"):
  1162. await team.run(task=["not a message"]) # type: ignore[list-item, arg-type] # intentionally testing invalid input
  1163. # Test with empty message list
  1164. with pytest.raises(ValueError, match="Task list cannot be empty"):
  1165. await team.run(task=[])
  1166. @pytest.mark.asyncio
  1167. async def test_declarative_groupchats_with_config() -> None:
  1168. # Create basic agents and components for testing
  1169. agent1 = AssistantAgent(
  1170. "agent_1",
  1171. model_client=OpenAIChatCompletionClient(model="gpt-4o-2024-05-13", api_key=""),
  1172. handoffs=["agent_2"],
  1173. )
  1174. agent2 = AssistantAgent("agent_2", model_client=OpenAIChatCompletionClient(model="gpt-4o-2024-05-13", api_key=""))
  1175. termination = MaxMessageTermination(4)
  1176. model_client = OpenAIChatCompletionClient(model="gpt-4o-2024-05-13", api_key="")
  1177. # Test round robin - verify config is preserved
  1178. round_robin = RoundRobinGroupChat(participants=[agent1, agent2], termination_condition=termination, max_turns=5)
  1179. config = round_robin.dump_component()
  1180. loaded = RoundRobinGroupChat.load_component(config)
  1181. assert loaded.dump_component() == config
  1182. # Test selector group chat - verify config is preserved
  1183. selector_prompt = "Custom selector prompt with {roles}, {participants}, {history}"
  1184. selector = SelectorGroupChat(
  1185. participants=[agent1, agent2],
  1186. model_client=model_client,
  1187. termination_condition=termination,
  1188. max_turns=10,
  1189. selector_prompt=selector_prompt,
  1190. allow_repeated_speaker=True,
  1191. )
  1192. selector_config = selector.dump_component()
  1193. selector_loaded = SelectorGroupChat.load_component(selector_config)
  1194. assert selector_loaded.dump_component() == selector_config
  1195. # Test swarm with handoff termination
  1196. handoff_termination = HandoffTermination(target="Agent2")
  1197. swarm = Swarm(participants=[agent1, agent2], termination_condition=handoff_termination, max_turns=5)
  1198. swarm_config = swarm.dump_component()
  1199. swarm_loaded = Swarm.load_component(swarm_config)
  1200. assert swarm_loaded.dump_component() == swarm_config
  1201. # Test MagenticOne with custom parameters
  1202. magentic = MagenticOneGroupChat(
  1203. participants=[agent1],
  1204. model_client=model_client,
  1205. max_turns=15,
  1206. max_stalls=5,
  1207. final_answer_prompt="Custom prompt",
  1208. )
  1209. magentic_config = magentic.dump_component()
  1210. magentic_loaded = MagenticOneGroupChat.load_component(magentic_config)
  1211. assert magentic_loaded.dump_component() == magentic_config
  1212. # Verify component types are correctly set for each
  1213. for team in [loaded, selector, swarm, magentic]:
  1214. assert team.component_type == "team"
  1215. # Verify provider strings are correctly set
  1216. assert round_robin.dump_component().provider == "autogen_agentchat.teams.RoundRobinGroupChat"
  1217. assert selector.dump_component().provider == "autogen_agentchat.teams.SelectorGroupChat"
  1218. assert swarm.dump_component().provider == "autogen_agentchat.teams.Swarm"
  1219. assert magentic.dump_component().provider == "autogen_agentchat.teams.MagenticOneGroupChat"