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_function_and_tool_calling.py 14 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. import json
  2. from typing import Any, Callable, Dict, List
  3. import pytest
  4. from autogen.agentchat.conversable_agent import ConversableAgent
  5. def _tool_func_1(arg1: str, arg2: str) -> str:
  6. return f"_tool_func_1: {arg1} {arg2}"
  7. def _tool_func_2(arg1: str, arg2: str) -> str:
  8. return f"_tool_func_2: {arg1} {arg2}"
  9. def _tool_func_error(arg1: str, arg2: str) -> str:
  10. raise RuntimeError("Error in tool function")
  11. async def _a_tool_func_1(arg1: str, arg2: str) -> str:
  12. return f"_tool_func_1: {arg1} {arg2}"
  13. async def _a_tool_func_2(arg1: str, arg2: str) -> str:
  14. return f"_tool_func_2: {arg1} {arg2}"
  15. async def _a_tool_func_error(arg1: str, arg2: str) -> str:
  16. raise RuntimeError("Error in tool function")
  17. _tool_use_message_1 = {
  18. "role": "assistant",
  19. "content": None,
  20. "function_call": None,
  21. "tool_calls": [
  22. {
  23. "id": "1",
  24. "type": "function",
  25. "function": {
  26. "name": "_tool_func_1",
  27. "arguments": json.dumps({"arg1": "value1", "arg2": "value2"}),
  28. },
  29. },
  30. {
  31. "id": "2",
  32. "type": "function",
  33. "function": {
  34. "name": "_tool_func_2",
  35. "arguments": json.dumps({"arg1": "value3", "arg2": "value4"}),
  36. },
  37. },
  38. ],
  39. }
  40. _tool_use_message_1_bad_json = {
  41. "role": "assistant",
  42. "content": None,
  43. "function_call": None,
  44. "tool_calls": [
  45. {
  46. "id": "1",
  47. "type": "function",
  48. "function": {
  49. "name": "_tool_func_1",
  50. # add extra comma to make json invalid
  51. "arguments": json.dumps({"arg1": "value3", "arg2": "value4"})[:-1] + ",}",
  52. },
  53. },
  54. {
  55. "id": "2",
  56. "type": "function",
  57. "function": {
  58. "name": "_tool_func_2",
  59. "arguments": json.dumps({"arg1": "value3", "arg2": "value4"}),
  60. },
  61. },
  62. ],
  63. }
  64. _tool_use_message_1_expected_reply = {
  65. "role": "tool",
  66. "tool_responses": [
  67. {"tool_call_id": "1", "role": "tool", "content": "_tool_func_1: value1 value2"},
  68. {"tool_call_id": "2", "role": "tool", "content": "_tool_func_2: value3 value4"},
  69. ],
  70. # "content": "Tool Call Id: 1\n_tool_func_1: value1 value2\n\nTool Call Id: 2\n_tool_func_2: value3 value4",
  71. "content": "_tool_func_1: value1 value2\n\n_tool_func_2: value3 value4",
  72. }
  73. _tool_use_message_1_bad_json_expected_reply = {
  74. "role": "tool",
  75. "tool_responses": [
  76. {
  77. "tool_call_id": "1",
  78. "role": "tool",
  79. "content": "Error: Expecting property name enclosed in double quotes: line 1 column 37 (char 36)\n The argument must be in JSON format.",
  80. },
  81. {"tool_call_id": "2", "role": "tool", "content": "_tool_func_2: value3 value4"},
  82. ],
  83. "content": "Error: Expecting property name enclosed in double quotes: line 1 column 37 (char 36)\n The argument must be in JSON format.\n\n_tool_func_2: value3 value4",
  84. }
  85. _tool_use_message_1_error_expected_reply = {
  86. "role": "tool",
  87. "tool_responses": [
  88. {"tool_call_id": "1", "role": "tool", "content": "_tool_func_1: value1 value2"},
  89. {
  90. "tool_call_id": "2",
  91. "role": "tool",
  92. "content": "Error: Error in tool function",
  93. },
  94. ],
  95. "content": "_tool_func_1: value1 value2\n\nError: Error in tool function",
  96. }
  97. _tool_use_message_1_not_found_expected_reply = {
  98. "role": "tool",
  99. "tool_responses": [
  100. {"tool_call_id": "1", "role": "tool", "content": "_tool_func_1: value1 value2"},
  101. {
  102. "tool_call_id": "2",
  103. "role": "tool",
  104. "content": "Error: Function _tool_func_2 not found.",
  105. },
  106. ],
  107. "content": "_tool_func_1: value1 value2\n\nError: Function _tool_func_2 not found.",
  108. }
  109. _function_use_message_1 = {
  110. "role": "assistant",
  111. "content": None,
  112. "function_call": {
  113. "name": "_tool_func_1",
  114. "arguments": json.dumps({"arg1": "value1", "arg2": "value2"}),
  115. },
  116. }
  117. _function_use_message_1_bad_json = {
  118. "role": "assistant",
  119. "content": None,
  120. "function_call": {
  121. "name": "_tool_func_1",
  122. "arguments": json.dumps({"arg1": "value1", "arg2": "value2"})[:-1] + ",}",
  123. },
  124. }
  125. _function_use_message_1_expected_reply = {
  126. "name": "_tool_func_1",
  127. "role": "function",
  128. "content": "_tool_func_1: value1 value2",
  129. }
  130. _function_use_message_1_bad_json_expected_reply = {
  131. "name": "_tool_func_1",
  132. "role": "function",
  133. "content": "Error: Expecting property name enclosed in double quotes: line 1 column 37 (char 36)\n The argument must be in JSON format.",
  134. }
  135. _function_use_message_1_error_expected_reply = {
  136. "name": "_tool_func_1",
  137. "role": "function",
  138. "content": "Error: Error in tool function",
  139. }
  140. _function_use_message_1_not_found_expected_reply = {
  141. "name": "_tool_func_1",
  142. "role": "function",
  143. "content": "Error: Function _tool_func_1 not found.",
  144. }
  145. _text_message = {"content": "Hi!", "role": "user"}
  146. def _get_function_map(is_function_async: bool, drop_tool_2: bool = False) -> Dict[str, Callable[..., Any]]:
  147. if is_function_async:
  148. return (
  149. {
  150. "_tool_func_1": _a_tool_func_1,
  151. "_tool_func_2": _a_tool_func_2,
  152. }
  153. if not drop_tool_2
  154. else {
  155. "_tool_func_1": _a_tool_func_1,
  156. }
  157. )
  158. else:
  159. return (
  160. {
  161. "_tool_func_1": _tool_func_1,
  162. "_tool_func_2": _tool_func_2,
  163. }
  164. if not drop_tool_2
  165. else {
  166. "_tool_func_1": _tool_func_1,
  167. }
  168. )
  169. def _get_error_function_map(
  170. is_function_async: bool, error_on_tool_func_2: bool = True
  171. ) -> Dict[str, Callable[..., Any]]:
  172. if is_function_async:
  173. return {
  174. "_tool_func_1": _a_tool_func_1 if error_on_tool_func_2 else _a_tool_func_error,
  175. "_tool_func_2": _a_tool_func_error if error_on_tool_func_2 else _a_tool_func_2,
  176. }
  177. else:
  178. return {
  179. "_tool_func_1": _tool_func_1 if error_on_tool_func_2 else _tool_func_error,
  180. "_tool_func_2": _tool_func_error if error_on_tool_func_2 else _tool_func_2,
  181. }
  182. @pytest.mark.parametrize("is_function_async", [True, False])
  183. def test_generate_function_call_reply_on_function_call_message(is_function_async: bool) -> None:
  184. agent = ConversableAgent(name="agent", llm_config=False)
  185. # empty function_map
  186. agent._function_map = {}
  187. messages = [_function_use_message_1]
  188. finished, retval = agent.generate_function_call_reply(messages)
  189. assert (finished, retval) == (True, _function_use_message_1_not_found_expected_reply)
  190. # function map set
  191. agent._function_map = _get_function_map(is_function_async)
  192. # correct function call, multiple times to make sure cleanups are done properly
  193. for _ in range(3):
  194. messages = [_function_use_message_1]
  195. finished, retval = agent.generate_function_call_reply(messages)
  196. assert (finished, retval) == (True, _function_use_message_1_expected_reply)
  197. # bad JSON
  198. messages = [_function_use_message_1_bad_json]
  199. finished, retval = agent.generate_function_call_reply(messages)
  200. assert (finished, retval) == (True, _function_use_message_1_bad_json_expected_reply)
  201. # tool call
  202. messages = [_tool_use_message_1]
  203. finished, retval = agent.generate_function_call_reply(messages)
  204. assert (finished, retval) == (False, None)
  205. # text message
  206. messages: List[Dict[str, str]] = [_text_message]
  207. finished, retval = agent.generate_function_call_reply(messages)
  208. assert (finished, retval) == (False, None)
  209. # error in function (raises Exception)
  210. agent._function_map = _get_error_function_map(is_function_async, error_on_tool_func_2=False)
  211. messages = [_function_use_message_1]
  212. finished, retval = agent.generate_function_call_reply(messages)
  213. assert (finished, retval) == (True, _function_use_message_1_error_expected_reply)
  214. @pytest.mark.asyncio()
  215. @pytest.mark.parametrize("is_function_async", [True, False])
  216. async def test_a_generate_function_call_reply_on_function_call_message(is_function_async: bool) -> None:
  217. agent = ConversableAgent(name="agent", llm_config=False)
  218. # empty function_map
  219. agent._function_map = {}
  220. messages = [_function_use_message_1]
  221. finished, retval = await agent.a_generate_function_call_reply(messages)
  222. assert (finished, retval) == (True, _function_use_message_1_not_found_expected_reply)
  223. # function map set
  224. agent._function_map = _get_function_map(is_function_async)
  225. # correct function call, multiple times to make sure cleanups are done properly
  226. for _ in range(3):
  227. messages = [_function_use_message_1]
  228. finished, retval = await agent.a_generate_function_call_reply(messages)
  229. assert (finished, retval) == (True, _function_use_message_1_expected_reply)
  230. # bad JSON
  231. messages = [_function_use_message_1_bad_json]
  232. finished, retval = await agent.a_generate_function_call_reply(messages)
  233. assert (finished, retval) == (True, _function_use_message_1_bad_json_expected_reply)
  234. # tool call
  235. messages = [_tool_use_message_1]
  236. finished, retval = await agent.a_generate_function_call_reply(messages)
  237. assert (finished, retval) == (False, None)
  238. # text message
  239. messages: List[Dict[str, str]] = [_text_message]
  240. finished, retval = await agent.a_generate_function_call_reply(messages)
  241. assert (finished, retval) == (False, None)
  242. # error in function (raises Exception)
  243. agent._function_map = _get_error_function_map(is_function_async, error_on_tool_func_2=False)
  244. messages = [_function_use_message_1]
  245. finished, retval = await agent.a_generate_function_call_reply(messages)
  246. assert (finished, retval) == (True, _function_use_message_1_error_expected_reply)
  247. @pytest.mark.parametrize("is_function_async", [True, False])
  248. def test_generate_tool_calls_reply_on_function_call_message(is_function_async: bool) -> None:
  249. agent = ConversableAgent(name="agent", llm_config=False)
  250. # empty function_map
  251. agent._function_map = _get_function_map(is_function_async, drop_tool_2=True)
  252. messages = [_tool_use_message_1]
  253. finished, retval = agent.generate_tool_calls_reply(messages)
  254. assert (finished, retval) == (True, _tool_use_message_1_not_found_expected_reply)
  255. # function map set
  256. agent._function_map = _get_function_map(is_function_async)
  257. # correct function call, multiple times to make sure cleanups are done properly
  258. for _ in range(3):
  259. messages = [_tool_use_message_1]
  260. finished, retval = agent.generate_tool_calls_reply(messages)
  261. assert (finished, retval) == (True, _tool_use_message_1_expected_reply)
  262. # bad JSON
  263. messages = [_tool_use_message_1_bad_json]
  264. finished, retval = agent.generate_tool_calls_reply(messages)
  265. assert (finished, retval) == (True, _tool_use_message_1_bad_json_expected_reply)
  266. # function call
  267. messages = [_function_use_message_1]
  268. finished, retval = agent.generate_tool_calls_reply(messages)
  269. assert (finished, retval) == (False, None)
  270. # text message
  271. messages: List[Dict[str, str]] = [_text_message]
  272. finished, retval = agent.generate_tool_calls_reply(messages)
  273. assert (finished, retval) == (False, None)
  274. # error in function (raises Exception)
  275. agent._function_map = _get_error_function_map(is_function_async)
  276. messages = [_tool_use_message_1]
  277. finished, retval = agent.generate_tool_calls_reply(messages)
  278. assert (finished, retval) == (True, _tool_use_message_1_error_expected_reply)
  279. @pytest.mark.asyncio()
  280. @pytest.mark.parametrize("is_function_async", [True, False])
  281. async def test_a_generate_tool_calls_reply_on_function_call_message(is_function_async: bool) -> None:
  282. agent = ConversableAgent(name="agent", llm_config=False)
  283. # empty function_map
  284. agent._function_map = _get_function_map(is_function_async, drop_tool_2=True)
  285. messages = [_tool_use_message_1]
  286. finished, retval = await agent.a_generate_tool_calls_reply(messages)
  287. assert (finished, retval) == (True, _tool_use_message_1_not_found_expected_reply)
  288. # function map set
  289. agent._function_map = _get_function_map(is_function_async)
  290. # correct function call, multiple times to make sure cleanups are done properly
  291. for _ in range(3):
  292. messages = [_tool_use_message_1]
  293. finished, retval = await agent.a_generate_tool_calls_reply(messages)
  294. assert (finished, retval) == (True, _tool_use_message_1_expected_reply)
  295. # bad JSON
  296. messages = [_tool_use_message_1_bad_json]
  297. finished, retval = await agent.a_generate_tool_calls_reply(messages)
  298. assert (finished, retval) == (True, _tool_use_message_1_bad_json_expected_reply)
  299. # function call
  300. messages = [_function_use_message_1]
  301. finished, retval = await agent.a_generate_tool_calls_reply(messages)
  302. assert (finished, retval) == (False, None)
  303. # text message
  304. messages: List[Dict[str, str]] = [_text_message]
  305. finished, retval = await agent.a_generate_tool_calls_reply(messages)
  306. assert (finished, retval) == (False, None)
  307. # error in function (raises Exception)
  308. agent._function_map = _get_error_function_map(is_function_async)
  309. messages = [_tool_use_message_1]
  310. finished, retval = await agent.a_generate_tool_calls_reply(messages)
  311. assert (finished, retval) == (True, _tool_use_message_1_error_expected_reply)