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

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