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_nested.py 26 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668
  1. import logging
  2. import tempfile
  3. from collections.abc import AsyncGenerator
  4. import pytest
  5. import pytest_asyncio
  6. from autogen_agentchat import EVENT_LOGGER_NAME
  7. from autogen_agentchat.agents import AssistantAgent, CodeExecutorAgent
  8. from autogen_agentchat.base import TaskResult
  9. from autogen_agentchat.conditions import MaxMessageTermination, TextMentionTermination
  10. from autogen_agentchat.messages import (
  11. BaseAgentEvent,
  12. BaseChatMessage,
  13. TextMessage,
  14. )
  15. from autogen_agentchat.teams import RoundRobinGroupChat, SelectorGroupChat, Swarm
  16. from autogen_core import AgentRuntime, SingleThreadedAgentRuntime
  17. from autogen_ext.code_executors.local import LocalCommandLineCodeExecutor
  18. from autogen_ext.models.replay import ReplayChatCompletionClient
  19. # Import test utilities from the main test file
  20. from utils import FileLogHandler
  21. logger = logging.getLogger(EVENT_LOGGER_NAME)
  22. logger.setLevel(logging.DEBUG)
  23. logger.addHandler(FileLogHandler("test_group_chat_nested.log"))
  24. @pytest_asyncio.fixture(params=["single_threaded", "embedded"]) # type: ignore
  25. async def runtime(request: pytest.FixtureRequest) -> AsyncGenerator[AgentRuntime | None, None]:
  26. if request.param == "single_threaded":
  27. runtime = SingleThreadedAgentRuntime()
  28. runtime.start()
  29. yield runtime
  30. await runtime.stop()
  31. elif request.param == "embedded":
  32. yield None
  33. @pytest.mark.asyncio
  34. async def test_round_robin_group_chat_nested_teams_run(runtime: AgentRuntime | None) -> None:
  35. """Test RoundRobinGroupChat with nested teams using run method."""
  36. model_client = ReplayChatCompletionClient(
  37. [
  38. 'Here is the program\n ```python\nprint("Hello, world!")\n```',
  39. "TERMINATE",
  40. "Good job",
  41. "TERMINATE",
  42. ],
  43. )
  44. with tempfile.TemporaryDirectory() as temp_dir:
  45. code_executor = LocalCommandLineCodeExecutor(work_dir=temp_dir)
  46. assistant = AssistantAgent(
  47. "assistant",
  48. model_client=model_client,
  49. description="An assistant agent that writes code.",
  50. )
  51. code_executor_agent = CodeExecutorAgent("code_executor", code_executor=code_executor)
  52. termination = TextMentionTermination("TERMINATE")
  53. # Create inner team (assistant + code executor)
  54. inner_team = RoundRobinGroupChat(
  55. participants=[assistant, code_executor_agent],
  56. termination_condition=termination,
  57. runtime=runtime,
  58. )
  59. # Create reviewer agent
  60. reviewer = AssistantAgent(
  61. "reviewer",
  62. model_client=model_client,
  63. description="A reviewer agent that reviews code.",
  64. )
  65. # Create outer team with nested inner team
  66. outer_team = RoundRobinGroupChat(
  67. participants=[inner_team, reviewer],
  68. termination_condition=termination,
  69. runtime=runtime,
  70. )
  71. result = await outer_team.run(task="Write a program that prints 'Hello, world!'")
  72. # Should have task message + inner team result + reviewer response + termination
  73. assert len(result.messages) >= 4
  74. assert isinstance(result.messages[0], TextMessage)
  75. assert result.messages[0].content == "Write a program that prints 'Hello, world!'"
  76. assert result.stop_reason is not None and "TERMINATE" in result.stop_reason
  77. @pytest.mark.asyncio
  78. async def test_round_robin_group_chat_nested_teams_run_stream(runtime: AgentRuntime | None) -> None:
  79. """Test RoundRobinGroupChat with nested teams using run_stream method."""
  80. model_client = ReplayChatCompletionClient(
  81. [
  82. 'Here is the program\n ```python\nprint("Hello, world!")\n```',
  83. "TERMINATE",
  84. "Good job",
  85. "TERMINATE",
  86. ],
  87. )
  88. with tempfile.TemporaryDirectory() as temp_dir:
  89. code_executor = LocalCommandLineCodeExecutor(work_dir=temp_dir)
  90. assistant = AssistantAgent(
  91. "assistant",
  92. model_client=model_client,
  93. description="An assistant agent that writes code.",
  94. )
  95. code_executor_agent = CodeExecutorAgent("code_executor", code_executor=code_executor)
  96. termination = TextMentionTermination("TERMINATE")
  97. # Create inner team (assistant + code executor)
  98. inner_team = RoundRobinGroupChat(
  99. participants=[assistant, code_executor_agent],
  100. termination_condition=termination,
  101. runtime=runtime,
  102. )
  103. # Create reviewer agent
  104. reviewer = AssistantAgent(
  105. "reviewer",
  106. model_client=model_client,
  107. description="A reviewer agent that reviews code.",
  108. )
  109. # Create outer team with nested inner team
  110. outer_team = RoundRobinGroupChat(
  111. participants=[inner_team, reviewer],
  112. termination_condition=termination,
  113. runtime=runtime,
  114. )
  115. messages: list[BaseAgentEvent | BaseChatMessage] = []
  116. result = None
  117. async for message in outer_team.run_stream(task="Write a program that prints 'Hello, world!'"):
  118. if isinstance(message, TaskResult):
  119. result = message
  120. else:
  121. messages.append(message)
  122. assert result is not None
  123. assert len(result.messages) >= 4
  124. assert isinstance(result.messages[0], TextMessage)
  125. assert result.messages[0].content == "Write a program that prints 'Hello, world!'"
  126. assert result.stop_reason is not None and "TERMINATE" in result.stop_reason
  127. @pytest.mark.asyncio
  128. async def test_round_robin_group_chat_nested_teams_dump_load_component(runtime: AgentRuntime | None) -> None:
  129. """Test RoundRobinGroupChat with nested teams dump_component and load_component."""
  130. model_client = ReplayChatCompletionClient(["Hello from agent1", "Hello from agent2", "Hello from agent3"])
  131. # Create agents
  132. agent1 = AssistantAgent("agent1", model_client=model_client, description="First agent")
  133. agent2 = AssistantAgent("agent2", model_client=model_client, description="Second agent")
  134. agent3 = AssistantAgent("agent3", model_client=model_client, description="Third agent")
  135. termination = MaxMessageTermination(2)
  136. # Create inner team
  137. inner_team = RoundRobinGroupChat(
  138. participants=[agent1, agent2],
  139. termination_condition=termination,
  140. runtime=runtime,
  141. name="InnerTeam",
  142. description="Inner team description",
  143. )
  144. # Create outer team with nested inner team
  145. outer_team = RoundRobinGroupChat(
  146. participants=[inner_team, agent3],
  147. termination_condition=termination,
  148. runtime=runtime,
  149. name="OuterTeam",
  150. description="Outer team description",
  151. )
  152. # Test dump_component
  153. config = outer_team.dump_component()
  154. assert config.config["name"] == "OuterTeam"
  155. assert config.config["description"] == "Outer team description"
  156. assert len(config.config["participants"]) == 2
  157. # First participant should be the inner team
  158. inner_team_config = config.config["participants"][0]["config"]
  159. assert inner_team_config["name"] == "InnerTeam"
  160. assert inner_team_config["description"] == "Inner team description"
  161. assert len(inner_team_config["participants"]) == 2
  162. # Second participant should be agent3
  163. agent3_config = config.config["participants"][1]["config"]
  164. assert agent3_config["name"] == "agent3"
  165. # Test load_component
  166. loaded_team = RoundRobinGroupChat.load_component(config)
  167. assert loaded_team.name == "OuterTeam"
  168. assert loaded_team.description == "Outer team description"
  169. assert len(loaded_team._participants) == 2 # type: ignore[reportPrivateUsage]
  170. # Verify the loaded team has the same structure
  171. loaded_config = loaded_team.dump_component()
  172. assert loaded_config == config
  173. @pytest.mark.asyncio
  174. async def test_round_robin_group_chat_nested_teams_save_load_state(runtime: AgentRuntime | None) -> None:
  175. """Test RoundRobinGroupChat with nested teams save_state and load_state."""
  176. model_client = ReplayChatCompletionClient(["Hello from agent1", "Hello from agent2", "TERMINATE"])
  177. # Create agents
  178. agent1 = AssistantAgent("agent1", model_client=model_client, description="First agent")
  179. agent2 = AssistantAgent("agent2", model_client=model_client, description="Second agent")
  180. agent3 = AssistantAgent("agent3", model_client=model_client, description="Third agent")
  181. termination = TextMentionTermination("TERMINATE") # Use TextMentionTermination
  182. # Create inner team
  183. inner_team = RoundRobinGroupChat(
  184. participants=[agent1, agent2],
  185. termination_condition=termination,
  186. runtime=runtime,
  187. )
  188. # Create outer team with nested inner team
  189. outer_team1 = RoundRobinGroupChat(
  190. participants=[inner_team, agent3],
  191. termination_condition=termination,
  192. runtime=runtime,
  193. )
  194. # Run the team to generate state
  195. await outer_team1.run(task="Test message")
  196. # Save state
  197. state = await outer_team1.save_state()
  198. # Create new agents and teams
  199. agent4 = AssistantAgent("agent1", model_client=model_client, description="First agent")
  200. agent5 = AssistantAgent("agent2", model_client=model_client, description="Second agent")
  201. agent6 = AssistantAgent("agent3", model_client=model_client, description="Third agent")
  202. inner_team2 = RoundRobinGroupChat(
  203. participants=[agent4, agent5],
  204. termination_condition=termination,
  205. runtime=runtime,
  206. )
  207. outer_team2 = RoundRobinGroupChat(
  208. participants=[inner_team2, agent6],
  209. termination_condition=termination,
  210. runtime=runtime,
  211. )
  212. # Load state
  213. await outer_team2.load_state(state)
  214. # Verify state was loaded correctly
  215. state2 = await outer_team2.save_state()
  216. assert state == state2
  217. @pytest.mark.asyncio
  218. async def test_selector_group_chat_nested_teams_run(runtime: AgentRuntime | None) -> None:
  219. """Test SelectorGroupChat with nested teams using run method."""
  220. model_client = ReplayChatCompletionClient(
  221. [
  222. "InnerTeam", # Select inner team first
  223. 'Here is the program\n ```python\nprint("Hello, world!")\n```',
  224. "TERMINATE",
  225. "agent3", # Select agent3 (reviewer)
  226. "Good job",
  227. "TERMINATE",
  228. ],
  229. )
  230. with tempfile.TemporaryDirectory() as temp_dir:
  231. code_executor = LocalCommandLineCodeExecutor(work_dir=temp_dir)
  232. assistant = AssistantAgent(
  233. "assistant",
  234. model_client=model_client,
  235. description="An assistant agent that writes code.",
  236. )
  237. code_executor_agent = CodeExecutorAgent("code_executor", code_executor=code_executor)
  238. termination = TextMentionTermination("TERMINATE")
  239. # Create inner team (assistant + code executor)
  240. inner_team = RoundRobinGroupChat(
  241. participants=[assistant, code_executor_agent],
  242. termination_condition=termination,
  243. runtime=runtime,
  244. name="InnerTeam",
  245. description="Team that writes and executes code",
  246. )
  247. # Create reviewer agent
  248. reviewer = AssistantAgent(
  249. "agent3",
  250. model_client=model_client,
  251. description="A reviewer agent that reviews code.",
  252. )
  253. # Create outer team with nested inner team
  254. outer_team = SelectorGroupChat(
  255. participants=[inner_team, reviewer],
  256. model_client=model_client,
  257. termination_condition=termination,
  258. runtime=runtime,
  259. )
  260. result = await outer_team.run(task="Write a program that prints 'Hello, world!'")
  261. # Should have task message + selector events + inner team result + reviewer response
  262. assert len(result.messages) >= 4
  263. assert isinstance(result.messages[0], TextMessage)
  264. assert result.messages[0].content == "Write a program that prints 'Hello, world!'"
  265. assert result.stop_reason is not None and "TERMINATE" in result.stop_reason
  266. @pytest.mark.asyncio
  267. async def test_selector_group_chat_nested_teams_run_stream(runtime: AgentRuntime | None) -> None:
  268. """Test SelectorGroupChat with nested teams using run_stream method."""
  269. model_client = ReplayChatCompletionClient(
  270. [
  271. "InnerTeam", # Select inner team first
  272. 'Here is the program\n ```python\nprint("Hello, world!")\n```',
  273. "TERMINATE",
  274. "agent3", # Select agent3 (reviewer)
  275. "Good job",
  276. "TERMINATE",
  277. ],
  278. )
  279. with tempfile.TemporaryDirectory() as temp_dir:
  280. code_executor = LocalCommandLineCodeExecutor(work_dir=temp_dir)
  281. assistant = AssistantAgent(
  282. "assistant",
  283. model_client=model_client,
  284. description="An assistant agent that writes code.",
  285. )
  286. code_executor_agent = CodeExecutorAgent("code_executor", code_executor=code_executor)
  287. termination = TextMentionTermination("TERMINATE")
  288. # Create inner team (assistant + code executor)
  289. inner_team = RoundRobinGroupChat(
  290. participants=[assistant, code_executor_agent],
  291. termination_condition=termination,
  292. runtime=runtime,
  293. name="InnerTeam",
  294. description="Team that writes and executes code",
  295. )
  296. # Create reviewer agent
  297. reviewer = AssistantAgent(
  298. "agent3",
  299. model_client=model_client,
  300. description="A reviewer agent that reviews code.",
  301. )
  302. # Create outer team with nested inner team
  303. outer_team = SelectorGroupChat(
  304. participants=[inner_team, reviewer],
  305. model_client=model_client,
  306. termination_condition=termination,
  307. runtime=runtime,
  308. )
  309. messages: list[BaseAgentEvent | BaseChatMessage] = []
  310. result = None
  311. async for message in outer_team.run_stream(task="Write a program that prints 'Hello, world!'"):
  312. if isinstance(message, TaskResult):
  313. result = message
  314. else:
  315. messages.append(message)
  316. assert result is not None
  317. assert len(result.messages) >= 4
  318. assert isinstance(result.messages[0], TextMessage)
  319. assert result.messages[0].content == "Write a program that prints 'Hello, world!'"
  320. assert result.stop_reason is not None and "TERMINATE" in result.stop_reason
  321. @pytest.mark.asyncio
  322. async def test_selector_group_chat_nested_teams_dump_load_component(runtime: AgentRuntime | None) -> None:
  323. """Test SelectorGroupChat with nested teams dump_component and load_component."""
  324. model_client = ReplayChatCompletionClient(["agent1", "Hello from agent1", "agent3", "Hello from agent3"])
  325. # Create agents
  326. agent1 = AssistantAgent("agent1", model_client=model_client, description="First agent")
  327. agent2 = AssistantAgent("agent2", model_client=model_client, description="Second agent")
  328. agent3 = AssistantAgent("agent3", model_client=model_client, description="Third agent")
  329. termination = MaxMessageTermination(2)
  330. # Create inner team
  331. inner_team = RoundRobinGroupChat(
  332. participants=[agent1, agent2],
  333. termination_condition=termination,
  334. runtime=runtime,
  335. name="InnerTeam",
  336. description="Inner team description",
  337. )
  338. # Create outer team with nested inner team
  339. outer_team = SelectorGroupChat(
  340. participants=[inner_team, agent3],
  341. model_client=model_client,
  342. termination_condition=termination,
  343. runtime=runtime,
  344. name="OuterTeam",
  345. description="Outer team description",
  346. )
  347. # Test dump_component
  348. config = outer_team.dump_component()
  349. assert config.config["name"] == "OuterTeam"
  350. assert config.config["description"] == "Outer team description"
  351. assert len(config.config["participants"]) == 2
  352. # First participant should be the inner team
  353. inner_team_config = config.config["participants"][0]["config"]
  354. assert inner_team_config["name"] == "InnerTeam"
  355. assert inner_team_config["description"] == "Inner team description"
  356. assert len(inner_team_config["participants"]) == 2
  357. # Second participant should be agent3
  358. agent3_config = config.config["participants"][1]["config"]
  359. assert agent3_config["name"] == "agent3"
  360. # Test load_component
  361. loaded_team = SelectorGroupChat.load_component(config)
  362. assert loaded_team.name == "OuterTeam"
  363. assert loaded_team.description == "Outer team description"
  364. assert len(loaded_team._participants) == 2 # type: ignore[reportPrivateUsage]
  365. # Verify the loaded team has the same structure
  366. loaded_config = loaded_team.dump_component()
  367. assert loaded_config == config
  368. @pytest.mark.asyncio
  369. async def test_selector_group_chat_nested_teams_save_load_state(runtime: AgentRuntime | None) -> None:
  370. """Test SelectorGroupChat with nested teams save_state and load_state."""
  371. model_client = ReplayChatCompletionClient(["InnerTeam", "Hello from inner team", "agent3", "TERMINATE"])
  372. # Create agents
  373. agent1 = AssistantAgent("agent1", model_client=model_client, description="First agent")
  374. agent2 = AssistantAgent("agent2", model_client=model_client, description="Second agent")
  375. agent3 = AssistantAgent("agent3", model_client=model_client, description="Third agent")
  376. termination = TextMentionTermination("TERMINATE")
  377. # Create inner team
  378. inner_team = RoundRobinGroupChat(
  379. participants=[agent1, agent2],
  380. termination_condition=termination,
  381. runtime=runtime,
  382. name="InnerTeam",
  383. )
  384. # Create outer team with nested inner team
  385. outer_team1 = SelectorGroupChat(
  386. participants=[inner_team, agent3],
  387. model_client=model_client,
  388. termination_condition=termination,
  389. runtime=runtime,
  390. )
  391. # Run the team to generate state
  392. await outer_team1.run(task="Test message")
  393. # Save state
  394. state = await outer_team1.save_state()
  395. # Create new agents and teams
  396. agent4 = AssistantAgent("agent1", model_client=model_client, description="First agent")
  397. agent5 = AssistantAgent("agent2", model_client=model_client, description="Second agent")
  398. agent6 = AssistantAgent("agent3", model_client=model_client, description="Third agent")
  399. inner_team2 = RoundRobinGroupChat(
  400. participants=[agent4, agent5],
  401. termination_condition=termination,
  402. runtime=runtime,
  403. name="InnerTeam",
  404. )
  405. outer_team2 = SelectorGroupChat(
  406. participants=[inner_team2, agent6],
  407. model_client=model_client,
  408. termination_condition=termination,
  409. runtime=runtime,
  410. )
  411. # Load state
  412. await outer_team2.load_state(state)
  413. # Verify state was loaded correctly
  414. state2 = await outer_team2.save_state()
  415. assert state == state2
  416. @pytest.mark.asyncio
  417. async def test_swarm_doesnt_support_nested_teams() -> None:
  418. """Test that Swarm raises TypeError when provided with nested teams."""
  419. model_client = ReplayChatCompletionClient(["Hello", "TERMINATE"])
  420. # Create agents
  421. agent1 = AssistantAgent("agent1", model_client=model_client, description="First agent")
  422. agent2 = AssistantAgent("agent2", model_client=model_client, description="Second agent")
  423. agent3 = AssistantAgent("agent3", model_client=model_client, description="Third agent")
  424. termination = TextMentionTermination("TERMINATE")
  425. # Create inner team
  426. inner_team = RoundRobinGroupChat(
  427. participants=[agent1, agent2],
  428. termination_condition=termination,
  429. )
  430. # Verify that Swarm raises TypeError when trying to use a team as participant
  431. with pytest.raises(TypeError, match="Participant .* must be a ChatAgent"):
  432. Swarm(
  433. participants=[inner_team, agent3], # type: ignore
  434. termination_condition=termination,
  435. )
  436. @pytest.mark.asyncio
  437. async def test_round_robin_deeply_nested_teams(runtime: AgentRuntime | None) -> None:
  438. """Test RoundRobinGroupChat with deeply nested teams (3 levels)."""
  439. model_client = ReplayChatCompletionClient(
  440. [
  441. "Hello from agent1",
  442. "TERMINATE from agent2",
  443. "World from agent3",
  444. "Hello from agent1",
  445. "Hello from agent2",
  446. "TERMINATE from agent1",
  447. "TERMINATE from agent3",
  448. "Review from agent4",
  449. "TERMINATE from agent2",
  450. "TERMINATE from agent3",
  451. "TERMINATE from agent4",
  452. ]
  453. )
  454. # Create agents
  455. agent1 = AssistantAgent("agent1", model_client=model_client, description="First agent")
  456. agent2 = AssistantAgent("agent2", model_client=model_client, description="Second agent")
  457. agent3 = AssistantAgent("agent3", model_client=model_client, description="Third agent")
  458. agent4 = AssistantAgent("agent4", model_client=model_client, description="Fourth agent")
  459. # Create innermost team (level 1)
  460. innermost_team = RoundRobinGroupChat(
  461. participants=[agent1, agent2],
  462. termination_condition=TextMentionTermination("TERMINATE", sources=["agent1", "agent2"]),
  463. runtime=runtime,
  464. name="InnermostTeam",
  465. )
  466. # Create middle team (level 2)
  467. middle_team = RoundRobinGroupChat(
  468. participants=[innermost_team, agent3],
  469. termination_condition=TextMentionTermination("TERMINATE", sources=["agent3"]),
  470. runtime=runtime,
  471. name="MiddleTeam",
  472. )
  473. # Create outermost team (level 3)
  474. outermost_team = RoundRobinGroupChat(
  475. participants=[middle_team, agent4],
  476. termination_condition=TextMentionTermination("TERMINATE", sources=["agent4"]),
  477. runtime=runtime,
  478. name="OutermostTeam",
  479. )
  480. result: TaskResult | None = None
  481. async for msg in outermost_team.run_stream(task="Test deep nesting"):
  482. if isinstance(msg, TaskResult):
  483. result = msg
  484. assert result is not None
  485. # Should have task message + responses from each level
  486. assert len(result.messages) == 12
  487. assert isinstance(result.messages[0], TextMessage)
  488. assert result.messages[0].content == "Test deep nesting"
  489. assert result.stop_reason is not None and "TERMINATE" in result.stop_reason
  490. # Test component serialization of deeply nested structure
  491. config = outermost_team.dump_component()
  492. loaded_team = RoundRobinGroupChat.load_component(config)
  493. assert loaded_team.name == "OutermostTeam"
  494. # Verify nested structure is preserved
  495. loaded_config = loaded_team.dump_component()
  496. assert loaded_config == config
  497. @pytest.mark.asyncio
  498. async def test_selector_deeply_nested_teams(runtime: AgentRuntime | None) -> None:
  499. """Test SelectorGroupChat with deeply nested teams (3 levels)."""
  500. model_client_inner = ReplayChatCompletionClient(
  501. [
  502. "Hello from innermost agent 1",
  503. "Hello from innermost agent 2",
  504. "TERMINATE from innermost agent 1",
  505. ]
  506. )
  507. model_client_middle = ReplayChatCompletionClient(
  508. [
  509. "InnermostTeam", # Select innermost team
  510. "TERMINATE from agent3",
  511. ]
  512. )
  513. model_client_outter = ReplayChatCompletionClient(
  514. [
  515. "MiddleTeam", # Select middle team
  516. "agent4", # Select agent4
  517. "Hello from outermost agent 4",
  518. "agent4", # Select agent4 again
  519. "TERMINATE from agent4",
  520. ]
  521. )
  522. # Create agents
  523. agent1 = AssistantAgent("agent1", model_client=model_client_inner, description="First agent")
  524. agent2 = AssistantAgent("agent2", model_client=model_client_inner, description="Second agent")
  525. agent3 = AssistantAgent("agent3", model_client=model_client_middle, description="Third agent")
  526. agent4 = AssistantAgent("agent4", model_client=model_client_outter, description="Fourth agent")
  527. # Create innermost team (level 1) - RoundRobin for simplicity
  528. innermost_team = RoundRobinGroupChat(
  529. participants=[agent1, agent2],
  530. termination_condition=TextMentionTermination("TERMINATE", sources=["agent1", "agent2"]),
  531. runtime=runtime,
  532. name="InnermostTeam",
  533. )
  534. # Create middle team (level 2) - Selector
  535. middle_team = SelectorGroupChat(
  536. participants=[innermost_team, agent3],
  537. model_client=model_client_middle,
  538. termination_condition=TextMentionTermination("TERMINATE", sources=["agent3"]),
  539. runtime=runtime,
  540. name="MiddleTeam",
  541. )
  542. # Create outermost team (level 3) - Selector
  543. outermost_team = SelectorGroupChat(
  544. participants=[middle_team, agent4],
  545. model_client=model_client_outter,
  546. termination_condition=TextMentionTermination("TERMINATE", sources=["agent4"]),
  547. runtime=runtime,
  548. name="OutermostTeam",
  549. allow_repeated_speaker=True,
  550. )
  551. result: TaskResult | None = None
  552. async for msg in outermost_team.run_stream(task="Test deep nesting"):
  553. if isinstance(msg, TaskResult):
  554. result = msg
  555. assert result is not None
  556. # Should have task message + selector events + responses from each level
  557. assert len(result.messages) == 7
  558. assert isinstance(result.messages[0], TextMessage)
  559. assert result.messages[0].content == "Test deep nesting"
  560. assert result.stop_reason is not None and "TERMINATE" in result.stop_reason
  561. # Test component serialization of deeply nested structure
  562. config = outermost_team.dump_component()
  563. loaded_team = SelectorGroupChat.load_component(config)
  564. assert loaded_team.name == "OutermostTeam"
  565. # Verify nested structure is preserved
  566. loaded_config = loaded_team.dump_component()
  567. assert loaded_config == config