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_pause_resume.py 4.4 kB

feat: Pause and Resume for AgentChat Teams and Agents (#5887) 1. Add `on_pause` and `on_resume` API to `ChatAgent` to support pausing behavior when running `on_message` concurrently. 2. Add `GroupChatPause` and `GroupChatResume` RPC events and handle them in `ChatAgentContainer`. 3. Add `pause` and `resume` API to `BaseGroupChat` to allow for this behavior accessible from the public API. 4. Improve `SequentialRoutedAgent` class to customize which message types are sequentially handled, making it possible to have concurrent handling for some messages (e.g., `GroupChatPause`). 5. Added unit tests. See `test_group_chat_pause_resume.py` for how to use this feature. What is the difference between pause/resume vs. termination and restart? - Pause and resume issue direct RPC calls to the participanting agents of a team while they are running, allowing putting the on-going generation or actions on hold. This is useful when an agent's turn takes a long time and multiple steps to complete, and user/application wants to re-evaluate whether it is worth continue the step or cancel. This also allows user/application to pause individual agents and resuming them independently from the team API. - Termination and restart requires the whole team to comes to a full-stop, and termination conditions are checked in between agents' turns. So termination can only happen when no agent is working on its turn. It is possible that a termination condition has reached well before the team is terminated, if the agent is taking a long time to generate a response. Resolves: #5881
1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. import asyncio
  2. from typing import AsyncGenerator, List, Sequence
  3. import pytest
  4. import pytest_asyncio
  5. from autogen_agentchat.agents import BaseChatAgent
  6. from autogen_agentchat.base import Response
  7. from autogen_agentchat.messages import ChatMessage, TextMessage
  8. from autogen_agentchat.teams import RoundRobinGroupChat
  9. from autogen_core import AgentRuntime, CancellationToken, SingleThreadedAgentRuntime
  10. class TestAgent(BaseChatAgent):
  11. """A test agent that does nothing."""
  12. def __init__(self, name: str, description: str) -> None:
  13. super().__init__(name=name, description=description)
  14. self._is_paused = False
  15. self._tasks: List[asyncio.Task[None]] = []
  16. self.counter = 0
  17. @property
  18. def produced_message_types(self) -> Sequence[type[ChatMessage]]:
  19. return [TextMessage]
  20. async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken) -> Response:
  21. assert not self._is_paused, "Agent is paused"
  22. async def _process() -> None:
  23. # Simulate a repetitive task that runs forever.
  24. while True:
  25. if self._is_paused:
  26. await asyncio.sleep(0.1)
  27. continue
  28. else:
  29. # Simulate a I/O operation that takes time, e.g., a browser operation.
  30. await asyncio.sleep(0.1)
  31. self.counter += 1
  32. curr_task = asyncio.create_task(_process())
  33. self._tasks.append(curr_task)
  34. try:
  35. # This will never return until the task is cancelled, at which point it will
  36. # raise an exception.
  37. await curr_task
  38. except asyncio.CancelledError:
  39. # The task was cancelled, so we can safely ignore this.
  40. pass
  41. return Response(
  42. chat_message=TextMessage(
  43. source=self.name,
  44. content="",
  45. ),
  46. )
  47. async def on_reset(self, cancellation_token: CancellationToken) -> None:
  48. self.counter = 0
  49. async def on_pause(self, cancellation_token: CancellationToken) -> None:
  50. self._is_paused = True
  51. async def on_resume(self, cancellation_token: CancellationToken) -> None:
  52. self._is_paused = False
  53. async def close(self) -> None:
  54. # Cancel all tasks and wait for them to finish.
  55. while self._tasks:
  56. task = self._tasks.pop()
  57. task.cancel()
  58. try:
  59. await task
  60. except asyncio.CancelledError:
  61. pass
  62. @pytest_asyncio.fixture(params=["single_threaded", "embedded"]) # type: ignore
  63. async def runtime(request: pytest.FixtureRequest) -> AsyncGenerator[AgentRuntime | None, None]:
  64. if request.param == "single_threaded":
  65. runtime = SingleThreadedAgentRuntime()
  66. runtime.start()
  67. yield runtime
  68. await runtime.stop()
  69. elif request.param == "embedded":
  70. yield None
  71. @pytest.mark.asyncio
  72. async def test_group_chat_pause_resume(runtime: AgentRuntime | None) -> None:
  73. agent = TestAgent(name="test_agent", description="test agent")
  74. team = RoundRobinGroupChat([agent], runtime=runtime, max_turns=1)
  75. # Run the team in a separate task.
  76. team_task = asyncio.create_task(team.run())
  77. # Get the current counter.
  78. curr_counter = agent.counter
  79. # Let the agent process the counter for a while.
  80. await asyncio.sleep(1)
  81. # Check that the agent's counter has increased.
  82. assert curr_counter < agent.counter
  83. curr_counter = agent.counter
  84. # Pause the team.
  85. await team.pause()
  86. # Wait for a while for the agent to process the pause.
  87. await asyncio.sleep(1)
  88. # Get the current counter value.
  89. curr_counter = agent.counter
  90. # Wait for a while.
  91. await asyncio.sleep(1)
  92. # Check that the agent's counter has not increased.
  93. assert curr_counter == agent.counter
  94. # Resume the agent.
  95. await team.resume()
  96. # Wait for a while for the agent to process the resume.
  97. await asyncio.sleep(1)
  98. # Get the current counter value.
  99. curr_counter = agent.counter
  100. # Wait for a while.
  101. await asyncio.sleep(1)
  102. # Check that the agent's counter has increased.
  103. assert curr_counter < agent.counter
  104. # Clean up -- force the agent to respond and terminate the team.
  105. await agent.close()
  106. # Wait for the team to terminate.
  107. await team_task