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_tools.py 15 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. import inspect
  2. from typing import Annotated, List
  3. import pytest
  4. from autogen_core import CancellationToken
  5. from autogen_core._function_utils import get_typed_signature
  6. from autogen_core.tools import BaseTool, FunctionTool
  7. from autogen_core.tools._base import ToolSchema
  8. from pydantic import BaseModel, Field, model_serializer
  9. from pydantic_core import PydanticUndefined
  10. class MyArgs(BaseModel):
  11. query: str = Field(description="The description.")
  12. class MyNestedArgs(BaseModel):
  13. arg: MyArgs = Field(description="The nested description.")
  14. class MyResult(BaseModel):
  15. result: str = Field(description="The other description.")
  16. class MyTool(BaseTool[MyArgs, MyResult]):
  17. def __init__(self) -> None:
  18. super().__init__(
  19. args_type=MyArgs,
  20. return_type=MyResult,
  21. name="TestTool",
  22. description="Description of test tool.",
  23. )
  24. self.called_count = 0
  25. async def run(self, args: MyArgs, cancellation_token: CancellationToken) -> MyResult:
  26. self.called_count += 1
  27. return MyResult(result="value")
  28. class MyNestedTool(BaseTool[MyNestedArgs, MyResult]):
  29. def __init__(self) -> None:
  30. super().__init__(
  31. args_type=MyNestedArgs,
  32. return_type=MyResult,
  33. name="TestNestedTool",
  34. description="Description of test nested tool.",
  35. )
  36. self.called_count = 0
  37. async def run(self, args: MyNestedArgs, cancellation_token: CancellationToken) -> MyResult:
  38. self.called_count += 1
  39. return MyResult(result="value")
  40. def test_tool_schema_generation() -> None:
  41. schema = MyTool().schema
  42. assert schema["name"] == "TestTool"
  43. assert "description" in schema
  44. assert schema["description"] == "Description of test tool."
  45. assert "parameters" in schema
  46. assert schema["parameters"]["type"] == "object"
  47. assert "properties" in schema["parameters"]
  48. assert schema["parameters"]["properties"]["query"]["description"] == "The description."
  49. assert schema["parameters"]["properties"]["query"]["type"] == "string"
  50. assert "required" in schema["parameters"]
  51. assert schema["parameters"]["required"] == ["query"]
  52. assert len(schema["parameters"]["properties"]) == 1
  53. def test_func_tool_schema_generation() -> None:
  54. def my_function(arg: str, other: Annotated[int, "int arg"], nonrequired: int = 5) -> MyResult:
  55. return MyResult(result="test")
  56. tool = FunctionTool(my_function, description="Function tool.")
  57. schema = tool.schema
  58. assert schema["name"] == "my_function"
  59. assert "description" in schema
  60. assert schema["description"] == "Function tool."
  61. assert "parameters" in schema
  62. assert schema["parameters"]["type"] == "object"
  63. assert schema["parameters"]["properties"].keys() == {"arg", "other", "nonrequired"}
  64. assert schema["parameters"]["properties"]["arg"]["type"] == "string"
  65. assert schema["parameters"]["properties"]["arg"]["description"] == "arg"
  66. assert schema["parameters"]["properties"]["other"]["type"] == "integer"
  67. assert schema["parameters"]["properties"]["other"]["description"] == "int arg"
  68. assert schema["parameters"]["properties"]["nonrequired"]["type"] == "integer"
  69. assert schema["parameters"]["properties"]["nonrequired"]["description"] == "nonrequired"
  70. assert "required" in schema["parameters"]
  71. assert schema["parameters"]["required"] == ["arg", "other"]
  72. assert len(schema["parameters"]["properties"]) == 3
  73. def test_func_tool_schema_generation_only_default_arg() -> None:
  74. def my_function(arg: str = "default") -> MyResult:
  75. return MyResult(result="test")
  76. tool = FunctionTool(my_function, description="Function tool.")
  77. schema = tool.schema
  78. assert schema["name"] == "my_function"
  79. assert "description" in schema
  80. assert schema["description"] == "Function tool."
  81. assert "parameters" in schema
  82. assert len(schema["parameters"]["properties"]) == 1
  83. assert schema["parameters"]["properties"]["arg"]["type"] == "string"
  84. assert schema["parameters"]["properties"]["arg"]["description"] == "arg"
  85. assert "required" not in schema["parameters"]
  86. @pytest.mark.asyncio
  87. async def test_tool_run() -> None:
  88. tool = MyTool()
  89. result = await tool.run_json({"query": "test"}, CancellationToken())
  90. assert isinstance(result, MyResult)
  91. assert result.result == "value"
  92. assert tool.called_count == 1
  93. result = await tool.run_json({"query": "test"}, CancellationToken())
  94. result = await tool.run_json({"query": "test"}, CancellationToken())
  95. assert tool.called_count == 3
  96. def test_tool_properties() -> None:
  97. tool = MyTool()
  98. assert tool.name == "TestTool"
  99. assert tool.description == "Description of test tool."
  100. assert tool.args_type() == MyArgs
  101. assert tool.return_type() == MyResult
  102. assert tool.state_type() is None
  103. def test_get_typed_signature() -> None:
  104. def my_function() -> str:
  105. return "result"
  106. sig = get_typed_signature(my_function)
  107. assert isinstance(sig, inspect.Signature)
  108. assert len(sig.parameters) == 0
  109. assert sig.return_annotation is str
  110. def test_get_typed_signature_annotated() -> None:
  111. def my_function() -> Annotated[str, "The return type"]:
  112. return "result"
  113. sig = get_typed_signature(my_function)
  114. assert isinstance(sig, inspect.Signature)
  115. assert len(sig.parameters) == 0
  116. assert sig.return_annotation == Annotated[str, "The return type"]
  117. def test_get_typed_signature_string() -> None:
  118. def my_function() -> "str":
  119. return "result"
  120. sig = get_typed_signature(my_function)
  121. assert isinstance(sig, inspect.Signature)
  122. assert len(sig.parameters) == 0
  123. assert sig.return_annotation is str
  124. def test_get_typed_signature_params() -> None:
  125. def my_function(arg: str) -> None:
  126. return None
  127. sig = get_typed_signature(my_function)
  128. assert isinstance(sig, inspect.Signature)
  129. assert sig.return_annotation is type(None)
  130. assert len(sig.parameters) == 1
  131. assert sig.parameters["arg"].annotation is str
  132. def test_get_typed_signature_two_params() -> None:
  133. def my_function(arg: str, arg2: int) -> None:
  134. return None
  135. sig = get_typed_signature(my_function)
  136. assert isinstance(sig, inspect.Signature)
  137. assert len(sig.parameters) == 2
  138. assert sig.parameters["arg"].annotation is str
  139. assert sig.parameters["arg2"].annotation is int
  140. def test_get_typed_signature_param_str() -> None:
  141. def my_function(arg: "str") -> None:
  142. return None
  143. sig = get_typed_signature(my_function)
  144. assert isinstance(sig, inspect.Signature)
  145. assert len(sig.parameters) == 1
  146. assert sig.parameters["arg"].annotation is str
  147. def test_get_typed_signature_param_annotated() -> None:
  148. def my_function(arg: Annotated[str, "An arg"]) -> None:
  149. return None
  150. sig = get_typed_signature(my_function)
  151. assert isinstance(sig, inspect.Signature)
  152. assert len(sig.parameters) == 1
  153. assert sig.parameters["arg"].annotation == Annotated[str, "An arg"]
  154. def test_func_tool() -> None:
  155. def my_function() -> str:
  156. return "result"
  157. tool = FunctionTool(my_function, description="Function tool.")
  158. assert tool.name == "my_function"
  159. assert tool.description == "Function tool."
  160. assert issubclass(tool.args_type(), BaseModel)
  161. assert issubclass(tool.return_type(), str)
  162. assert tool.state_type() is None
  163. def test_func_tool_annotated_arg() -> None:
  164. def my_function(my_arg: Annotated[str, "test description"]) -> str:
  165. return "result"
  166. tool = FunctionTool(my_function, description="Function tool.")
  167. assert tool.name == "my_function"
  168. assert tool.description == "Function tool."
  169. assert issubclass(tool.args_type(), BaseModel)
  170. assert issubclass(tool.return_type(), str)
  171. assert tool.args_type().model_fields["my_arg"].description == "test description"
  172. assert tool.args_type().model_fields["my_arg"].annotation is str
  173. assert tool.args_type().model_fields["my_arg"].is_required() is True
  174. assert tool.args_type().model_fields["my_arg"].default is PydanticUndefined
  175. assert len(tool.args_type().model_fields) == 1
  176. assert tool.return_type() is str
  177. assert tool.state_type() is None
  178. def test_func_tool_return_annotated() -> None:
  179. def my_function() -> Annotated[str, "test description"]:
  180. return "result"
  181. tool = FunctionTool(my_function, description="Function tool.")
  182. assert tool.name == "my_function"
  183. assert tool.description == "Function tool."
  184. assert issubclass(tool.args_type(), BaseModel)
  185. assert tool.return_type() is str
  186. assert tool.state_type() is None
  187. def test_func_tool_no_args() -> None:
  188. def my_function() -> str:
  189. return "result"
  190. tool = FunctionTool(my_function, description="Function tool.")
  191. assert tool.name == "my_function"
  192. assert tool.description == "Function tool."
  193. assert issubclass(tool.args_type(), BaseModel)
  194. assert len(tool.args_type().model_fields) == 0
  195. assert tool.return_type() is str
  196. assert tool.state_type() is None
  197. def test_func_tool_return_none() -> None:
  198. def my_function() -> None:
  199. return None
  200. tool = FunctionTool(my_function, description="Function tool.")
  201. assert tool.name == "my_function"
  202. assert tool.description == "Function tool."
  203. assert issubclass(tool.args_type(), BaseModel)
  204. assert tool.return_type() is type(None)
  205. assert tool.state_type() is None
  206. def test_func_tool_return_base_model() -> None:
  207. def my_function() -> MyResult:
  208. return MyResult(result="value")
  209. tool = FunctionTool(my_function, description="Function tool.")
  210. assert tool.name == "my_function"
  211. assert tool.description == "Function tool."
  212. assert issubclass(tool.args_type(), BaseModel)
  213. assert tool.return_type() is MyResult
  214. assert tool.state_type() is None
  215. @pytest.mark.asyncio
  216. async def test_func_call_tool() -> None:
  217. def my_function() -> str:
  218. return "result"
  219. tool = FunctionTool(my_function, description="Function tool.")
  220. result = await tool.run_json({}, CancellationToken())
  221. assert result == "result"
  222. @pytest.mark.asyncio
  223. async def test_func_call_tool_base_model() -> None:
  224. def my_function() -> MyResult:
  225. return MyResult(result="value")
  226. tool = FunctionTool(my_function, description="Function tool.")
  227. result = await tool.run_json({}, CancellationToken())
  228. assert isinstance(result, MyResult)
  229. assert result.result == "value"
  230. @pytest.mark.asyncio
  231. async def test_func_call_tool_with_arg_base_model() -> None:
  232. def my_function(arg: str) -> MyResult:
  233. return MyResult(result="value")
  234. tool = FunctionTool(my_function, description="Function tool.")
  235. result = await tool.run_json({"arg": "test"}, CancellationToken())
  236. assert isinstance(result, MyResult)
  237. assert result.result == "value"
  238. @pytest.mark.asyncio
  239. async def test_func_str_res() -> None:
  240. def my_function(arg: str) -> str:
  241. return "test"
  242. tool = FunctionTool(my_function, description="Function tool.")
  243. result = await tool.run_json({"arg": "test"}, CancellationToken())
  244. assert tool.return_value_as_string(result) == "test"
  245. @pytest.mark.asyncio
  246. async def test_func_base_model_res() -> None:
  247. def my_function(arg: str) -> MyResult:
  248. return MyResult(result="test")
  249. tool = FunctionTool(my_function, description="Function tool.")
  250. result = await tool.run_json({"arg": "test"}, CancellationToken())
  251. assert tool.return_value_as_string(result) == '{"result": "test"}'
  252. @pytest.mark.asyncio
  253. async def test_func_base_model_custom_dump_res() -> None:
  254. class MyResultCustomDump(BaseModel):
  255. result: str = Field(description="The other description.")
  256. @model_serializer
  257. def ser_model(self) -> str:
  258. return "custom: " + self.result
  259. def my_function(arg: str) -> MyResultCustomDump:
  260. return MyResultCustomDump(result="test")
  261. tool = FunctionTool(my_function, description="Function tool.")
  262. result = await tool.run_json({"arg": "test"}, CancellationToken())
  263. assert tool.return_value_as_string(result) == "custom: test"
  264. @pytest.mark.asyncio
  265. async def test_func_int_res() -> None:
  266. def my_function(arg: int) -> int:
  267. return arg
  268. tool = FunctionTool(my_function, description="Function tool.")
  269. result = await tool.run_json({"arg": 5}, CancellationToken())
  270. assert tool.return_value_as_string(result) == "5"
  271. @pytest.mark.asyncio
  272. async def test_func_tool_return_list() -> None:
  273. def my_function() -> List[int]:
  274. return [1, 2]
  275. tool = FunctionTool(my_function, description="Function tool.")
  276. result = await tool.run_json({}, CancellationToken())
  277. assert isinstance(result, list)
  278. assert result == [1, 2]
  279. assert tool.return_value_as_string(result) == "[1, 2]"
  280. def test_nested_tool_schema_generation() -> None:
  281. schema: ToolSchema = MyNestedTool().schema
  282. assert "description" in schema
  283. assert "parameters" in schema
  284. assert "type" in schema["parameters"]
  285. assert "arg" in schema["parameters"]["properties"]
  286. assert "type" in schema["parameters"]["properties"]["arg"]
  287. assert "title" in schema["parameters"]["properties"]["arg"]
  288. assert "properties" in schema["parameters"]["properties"]["arg"]
  289. assert "query" in schema["parameters"]["properties"]["arg"]["properties"]
  290. assert "type" in schema["parameters"]["properties"]["arg"]["properties"]["query"]
  291. assert "description" in schema["parameters"]["properties"]["arg"]["properties"]["query"]
  292. assert "required" in schema["parameters"]
  293. assert schema["description"] == "Description of test nested tool."
  294. assert schema["parameters"]["type"] == "object"
  295. assert schema["parameters"]["properties"]["arg"]["type"] == "object"
  296. assert schema["parameters"]["properties"]["arg"]["title"] == "MyArgs"
  297. assert schema["parameters"]["properties"]["arg"]["properties"]["query"]["type"] == "string"
  298. assert schema["parameters"]["properties"]["arg"]["properties"]["query"]["description"] == "The description."
  299. assert schema["parameters"]["properties"]["arg"]["required"] == ["query"]
  300. assert schema["parameters"]["required"] == ["arg"]
  301. assert len(schema["parameters"]["properties"]) == 1
  302. @pytest.mark.asyncio
  303. async def test_nested_tool_run() -> None:
  304. tool = MyNestedTool()
  305. result = await tool.run_json({"arg": {"query": "test"}}, CancellationToken())
  306. assert isinstance(result, MyResult)
  307. assert result.result == "value"
  308. assert tool.called_count == 1
  309. result = await tool.run_json({"arg": {"query": "test"}}, CancellationToken())
  310. result = await tool.run_json({"arg": {"query": "test"}}, CancellationToken())
  311. assert tool.called_count == 3
  312. def test_nested_tool_properties() -> None:
  313. tool = MyNestedTool()
  314. assert tool.name == "TestNestedTool"
  315. assert tool.description == "Description of test nested tool."
  316. assert tool.args_type() == MyNestedArgs
  317. assert tool.return_type() == MyResult
  318. assert tool.state_type() is None