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 16 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  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. BaseChatAgent,
  10. ChatMessage,
  11. CodeExecutorAgent,
  12. CodingAssistantAgent,
  13. StopMessage,
  14. TextMessage,
  15. ToolUseAssistantAgent,
  16. )
  17. from autogen_agentchat.logging import FileLogHandler
  18. from autogen_agentchat.teams import (
  19. RoundRobinGroupChat,
  20. SelectorGroupChat,
  21. StopMessageTermination,
  22. )
  23. from autogen_core.base import CancellationToken
  24. from autogen_core.components import FunctionCall
  25. from autogen_core.components.code_executor import LocalCommandLineCodeExecutor
  26. from autogen_core.components.models import FunctionExecutionResult, OpenAIChatCompletionClient
  27. from autogen_core.components.tools import FunctionTool
  28. from openai.resources.chat.completions import AsyncCompletions
  29. from openai.types.chat.chat_completion import ChatCompletion, Choice
  30. from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
  31. from openai.types.chat.chat_completion_message import ChatCompletionMessage
  32. from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall, Function
  33. from openai.types.completion_usage import CompletionUsage
  34. logger = logging.getLogger(EVENT_LOGGER_NAME)
  35. logger.setLevel(logging.DEBUG)
  36. logger.addHandler(FileLogHandler("test_group_chat.log"))
  37. class _MockChatCompletion:
  38. def __init__(self, chat_completions: List[ChatCompletion]) -> None:
  39. self._saved_chat_completions = chat_completions
  40. self._curr_index = 0
  41. async def mock_create(
  42. self, *args: Any, **kwargs: Any
  43. ) -> ChatCompletion | AsyncGenerator[ChatCompletionChunk, None]:
  44. await asyncio.sleep(0.1)
  45. completion = self._saved_chat_completions[self._curr_index]
  46. self._curr_index += 1
  47. return completion
  48. class _EchoAgent(BaseChatAgent):
  49. def __init__(self, name: str, description: str) -> None:
  50. super().__init__(name, description)
  51. self._last_message: str | None = None
  52. async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken) -> ChatMessage:
  53. if len(messages) > 0:
  54. assert isinstance(messages[0], TextMessage)
  55. self._last_message = messages[0].content
  56. return TextMessage(content=messages[0].content, source=self.name)
  57. else:
  58. assert self._last_message is not None
  59. return TextMessage(content=self._last_message, source=self.name)
  60. class _StopAgent(_EchoAgent):
  61. def __init__(self, name: str, description: str, *, stop_at: int = 1) -> None:
  62. super().__init__(name, description)
  63. self._count = 0
  64. self._stop_at = stop_at
  65. async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken) -> ChatMessage:
  66. self._count += 1
  67. if self._count < self._stop_at:
  68. return await super().on_messages(messages, cancellation_token)
  69. return StopMessage(content="TERMINATE", source=self.name)
  70. def _pass_function(input: str) -> str:
  71. return "pass"
  72. @pytest.mark.asyncio
  73. async def test_round_robin_group_chat(monkeypatch: pytest.MonkeyPatch) -> None:
  74. model = "gpt-4o-2024-05-13"
  75. chat_completions = [
  76. ChatCompletion(
  77. id="id1",
  78. choices=[
  79. Choice(
  80. finish_reason="stop",
  81. index=0,
  82. message=ChatCompletionMessage(
  83. content="""Here is the program\n ```python\nprint("Hello, world!")\n```""",
  84. role="assistant",
  85. ),
  86. )
  87. ],
  88. created=0,
  89. model=model,
  90. object="chat.completion",
  91. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  92. ),
  93. ChatCompletion(
  94. id="id2",
  95. choices=[
  96. Choice(
  97. finish_reason="stop",
  98. index=0,
  99. message=ChatCompletionMessage(content="TERMINATE", role="assistant"),
  100. )
  101. ],
  102. created=0,
  103. model=model,
  104. object="chat.completion",
  105. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  106. ),
  107. ]
  108. mock = _MockChatCompletion(chat_completions)
  109. monkeypatch.setattr(AsyncCompletions, "create", mock.mock_create)
  110. with tempfile.TemporaryDirectory() as temp_dir:
  111. code_executor_agent = CodeExecutorAgent(
  112. "code_executor", code_executor=LocalCommandLineCodeExecutor(work_dir=temp_dir)
  113. )
  114. coding_assistant_agent = CodingAssistantAgent(
  115. "coding_assistant", model_client=OpenAIChatCompletionClient(model=model, api_key="")
  116. )
  117. team = RoundRobinGroupChat(participants=[coding_assistant_agent, code_executor_agent])
  118. result = await team.run(
  119. "Write a program that prints 'Hello, world!'", termination_condition=StopMessageTermination()
  120. )
  121. expected_messages = [
  122. "Write a program that prints 'Hello, world!'",
  123. 'Here is the program\n ```python\nprint("Hello, world!")\n```',
  124. "Hello, world!",
  125. "TERMINATE",
  126. ]
  127. # Normalize the messages to remove \r\n and any leading/trailing whitespace.
  128. normalized_messages = [
  129. msg.content.replace("\r\n", "\n").rstrip("\n") if isinstance(msg.content, str) else msg.content
  130. for msg in result.messages
  131. ]
  132. # Assert that all expected messages are in the collected messages
  133. assert normalized_messages == expected_messages
  134. @pytest.mark.asyncio
  135. async def test_round_robin_group_chat_with_tools(monkeypatch: pytest.MonkeyPatch) -> None:
  136. model = "gpt-4o-2024-05-13"
  137. chat_completions = [
  138. ChatCompletion(
  139. id="id1",
  140. choices=[
  141. Choice(
  142. finish_reason="tool_calls",
  143. index=0,
  144. message=ChatCompletionMessage(
  145. content=None,
  146. tool_calls=[
  147. ChatCompletionMessageToolCall(
  148. id="1",
  149. type="function",
  150. function=Function(
  151. name="pass",
  152. arguments=json.dumps({"input": "pass"}),
  153. ),
  154. )
  155. ],
  156. role="assistant",
  157. ),
  158. )
  159. ],
  160. created=0,
  161. model=model,
  162. object="chat.completion",
  163. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  164. ),
  165. ChatCompletion(
  166. id="id2",
  167. choices=[
  168. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="Hello", role="assistant"))
  169. ],
  170. created=0,
  171. model=model,
  172. object="chat.completion",
  173. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  174. ),
  175. ChatCompletion(
  176. id="id2",
  177. choices=[
  178. Choice(
  179. finish_reason="stop", index=0, message=ChatCompletionMessage(content="TERMINATE", role="assistant")
  180. )
  181. ],
  182. created=0,
  183. model=model,
  184. object="chat.completion",
  185. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  186. ),
  187. ]
  188. mock = _MockChatCompletion(chat_completions)
  189. monkeypatch.setattr(AsyncCompletions, "create", mock.mock_create)
  190. tool = FunctionTool(_pass_function, name="pass", description="pass function")
  191. tool_use_agent = ToolUseAssistantAgent(
  192. "tool_use_agent",
  193. model_client=OpenAIChatCompletionClient(model=model, api_key=""),
  194. registered_tools=[tool],
  195. )
  196. echo_agent = _EchoAgent("echo_agent", description="echo agent")
  197. team = RoundRobinGroupChat(participants=[tool_use_agent, echo_agent])
  198. await team.run("Write a program that prints 'Hello, world!'", termination_condition=StopMessageTermination())
  199. context = tool_use_agent._model_context # pyright: ignore
  200. assert context[0].content == "Write a program that prints 'Hello, world!'"
  201. assert isinstance(context[1].content, list)
  202. assert isinstance(context[1].content[0], FunctionCall)
  203. assert context[1].content[0].name == "pass"
  204. assert context[1].content[0].arguments == json.dumps({"input": "pass"})
  205. assert isinstance(context[2].content, list)
  206. assert isinstance(context[2].content[0], FunctionExecutionResult)
  207. assert context[2].content[0].content == "pass"
  208. assert context[2].content[0].call_id == "1"
  209. assert context[3].content == "Hello"
  210. @pytest.mark.asyncio
  211. async def test_selector_group_chat(monkeypatch: pytest.MonkeyPatch) -> None:
  212. model = "gpt-4o-2024-05-13"
  213. chat_completions = [
  214. ChatCompletion(
  215. id="id2",
  216. choices=[
  217. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="agent3", role="assistant"))
  218. ],
  219. created=0,
  220. model=model,
  221. object="chat.completion",
  222. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  223. ),
  224. ChatCompletion(
  225. id="id2",
  226. choices=[
  227. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="agent2", role="assistant"))
  228. ],
  229. created=0,
  230. model=model,
  231. object="chat.completion",
  232. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  233. ),
  234. ChatCompletion(
  235. id="id2",
  236. choices=[
  237. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="agent1", role="assistant"))
  238. ],
  239. created=0,
  240. model=model,
  241. object="chat.completion",
  242. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  243. ),
  244. ChatCompletion(
  245. id="id2",
  246. choices=[
  247. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="agent2", role="assistant"))
  248. ],
  249. created=0,
  250. model=model,
  251. object="chat.completion",
  252. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  253. ),
  254. ChatCompletion(
  255. id="id2",
  256. choices=[
  257. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="agent1", role="assistant"))
  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. ]
  265. mock = _MockChatCompletion(chat_completions)
  266. monkeypatch.setattr(AsyncCompletions, "create", mock.mock_create)
  267. agent1 = _StopAgent("agent1", description="echo agent 1", stop_at=2)
  268. agent2 = _EchoAgent("agent2", description="echo agent 2")
  269. agent3 = _EchoAgent("agent3", description="echo agent 3")
  270. team = SelectorGroupChat(
  271. participants=[agent1, agent2, agent3],
  272. model_client=OpenAIChatCompletionClient(model=model, api_key=""),
  273. )
  274. result = await team.run(
  275. "Write a program that prints 'Hello, world!'", termination_condition=StopMessageTermination()
  276. )
  277. assert len(result.messages) == 6
  278. assert result.messages[0].content == "Write a program that prints 'Hello, world!'"
  279. assert result.messages[1].source == "agent3"
  280. assert result.messages[2].source == "agent2"
  281. assert result.messages[3].source == "agent1"
  282. assert result.messages[4].source == "agent2"
  283. assert result.messages[5].source == "agent1"
  284. @pytest.mark.asyncio
  285. async def test_selector_group_chat_two_speakers(monkeypatch: pytest.MonkeyPatch) -> None:
  286. model = "gpt-4o-2024-05-13"
  287. chat_completions = [
  288. ChatCompletion(
  289. id="id2",
  290. choices=[
  291. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="agent2", role="assistant"))
  292. ],
  293. created=0,
  294. model=model,
  295. object="chat.completion",
  296. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  297. ),
  298. ]
  299. mock = _MockChatCompletion(chat_completions)
  300. monkeypatch.setattr(AsyncCompletions, "create", mock.mock_create)
  301. agent1 = _StopAgent("agent1", description="echo agent 1", stop_at=2)
  302. agent2 = _EchoAgent("agent2", description="echo agent 2")
  303. team = SelectorGroupChat(
  304. participants=[agent1, agent2],
  305. model_client=OpenAIChatCompletionClient(model=model, api_key=""),
  306. )
  307. result = await team.run(
  308. "Write a program that prints 'Hello, world!'", termination_condition=StopMessageTermination()
  309. )
  310. assert len(result.messages) == 5
  311. assert result.messages[0].content == "Write a program that prints 'Hello, world!'"
  312. assert result.messages[1].source == "agent2"
  313. assert result.messages[2].source == "agent1"
  314. assert result.messages[3].source == "agent2"
  315. assert result.messages[4].source == "agent1"
  316. # only one chat completion was called
  317. assert mock._curr_index == 1 # pyright: ignore
  318. @pytest.mark.asyncio
  319. async def test_selector_group_chat_two_speakers_allow_repeated(monkeypatch: pytest.MonkeyPatch) -> None:
  320. model = "gpt-4o-2024-05-13"
  321. chat_completions = [
  322. ChatCompletion(
  323. id="id2",
  324. choices=[
  325. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="agent2", role="assistant"))
  326. ],
  327. created=0,
  328. model=model,
  329. object="chat.completion",
  330. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  331. ),
  332. ChatCompletion(
  333. id="id2",
  334. choices=[
  335. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="agent2", role="assistant"))
  336. ],
  337. created=0,
  338. model=model,
  339. object="chat.completion",
  340. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  341. ),
  342. ChatCompletion(
  343. id="id2",
  344. choices=[
  345. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="agent1", role="assistant"))
  346. ],
  347. created=0,
  348. model=model,
  349. object="chat.completion",
  350. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  351. ),
  352. ]
  353. mock = _MockChatCompletion(chat_completions)
  354. monkeypatch.setattr(AsyncCompletions, "create", mock.mock_create)
  355. agent1 = _StopAgent("agent1", description="echo agent 1", stop_at=1)
  356. agent2 = _EchoAgent("agent2", description="echo agent 2")
  357. team = SelectorGroupChat(
  358. participants=[agent1, agent2],
  359. model_client=OpenAIChatCompletionClient(model=model, api_key=""),
  360. allow_repeated_speaker=True,
  361. )
  362. result = await team.run(
  363. "Write a program that prints 'Hello, world!'", termination_condition=StopMessageTermination()
  364. )
  365. assert len(result.messages) == 4
  366. assert result.messages[0].content == "Write a program that prints 'Hello, world!'"
  367. assert result.messages[1].source == "agent2"
  368. assert result.messages[2].source == "agent2"
  369. assert result.messages[3].source == "agent1"