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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. import inspect
  2. from typing import Annotated, List
  3. import pytest
  4. from autogen_core.base import CancellationToken
  5. from autogen_core.components._function_utils import get_typed_signature
  6. from autogen_core.components.tools import BaseTool, FunctionTool
  7. from autogen_core.components.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 == 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 == str
  124. def test_func_tool() -> None:
  125. def my_function() -> str:
  126. return "result"
  127. tool = FunctionTool(my_function, description="Function tool.")
  128. assert tool.name == "my_function"
  129. assert tool.description == "Function tool."
  130. assert issubclass(tool.args_type(), BaseModel)
  131. assert issubclass(tool.return_type(), str)
  132. assert tool.state_type() is None
  133. def test_func_tool_annotated_arg() -> None:
  134. def my_function(my_arg: Annotated[str, "test description"]) -> str:
  135. return "result"
  136. tool = FunctionTool(my_function, description="Function tool.")
  137. assert tool.name == "my_function"
  138. assert tool.description == "Function tool."
  139. assert issubclass(tool.args_type(), BaseModel)
  140. assert issubclass(tool.return_type(), str)
  141. assert tool.args_type().model_fields["my_arg"].description == "test description"
  142. assert tool.args_type().model_fields["my_arg"].annotation == str
  143. assert tool.args_type().model_fields["my_arg"].is_required() is True
  144. assert tool.args_type().model_fields["my_arg"].default is PydanticUndefined
  145. assert len(tool.args_type().model_fields) == 1
  146. assert tool.return_type() == str
  147. assert tool.state_type() is None
  148. def test_func_tool_return_annotated() -> None:
  149. def my_function() -> Annotated[str, "test description"]:
  150. return "result"
  151. tool = FunctionTool(my_function, description="Function tool.")
  152. assert tool.name == "my_function"
  153. assert tool.description == "Function tool."
  154. assert issubclass(tool.args_type(), BaseModel)
  155. assert tool.return_type() == str
  156. assert tool.state_type() is None
  157. def test_func_tool_no_args() -> None:
  158. def my_function() -> str:
  159. return "result"
  160. tool = FunctionTool(my_function, description="Function tool.")
  161. assert tool.name == "my_function"
  162. assert tool.description == "Function tool."
  163. assert issubclass(tool.args_type(), BaseModel)
  164. assert len(tool.args_type().model_fields) == 0
  165. assert tool.return_type() == str
  166. assert tool.state_type() is None
  167. def test_func_tool_return_none() -> None:
  168. def my_function() -> None:
  169. return None
  170. tool = FunctionTool(my_function, description="Function tool.")
  171. assert tool.name == "my_function"
  172. assert tool.description == "Function tool."
  173. assert issubclass(tool.args_type(), BaseModel)
  174. assert tool.return_type() is None
  175. assert tool.state_type() is None
  176. def test_func_tool_return_base_model() -> None:
  177. def my_function() -> MyResult:
  178. return MyResult(result="value")
  179. tool = FunctionTool(my_function, description="Function tool.")
  180. assert tool.name == "my_function"
  181. assert tool.description == "Function tool."
  182. assert issubclass(tool.args_type(), BaseModel)
  183. assert tool.return_type() is MyResult
  184. assert tool.state_type() is None
  185. @pytest.mark.asyncio
  186. async def test_func_call_tool() -> None:
  187. def my_function() -> str:
  188. return "result"
  189. tool = FunctionTool(my_function, description="Function tool.")
  190. result = await tool.run_json({}, CancellationToken())
  191. assert result == "result"
  192. @pytest.mark.asyncio
  193. async def test_func_call_tool_base_model() -> None:
  194. def my_function() -> MyResult:
  195. return MyResult(result="value")
  196. tool = FunctionTool(my_function, description="Function tool.")
  197. result = await tool.run_json({}, CancellationToken())
  198. assert isinstance(result, MyResult)
  199. assert result.result == "value"
  200. @pytest.mark.asyncio
  201. async def test_func_call_tool_with_arg_base_model() -> None:
  202. def my_function(arg: str) -> MyResult:
  203. return MyResult(result="value")
  204. tool = FunctionTool(my_function, description="Function tool.")
  205. result = await tool.run_json({"arg": "test"}, CancellationToken())
  206. assert isinstance(result, MyResult)
  207. assert result.result == "value"
  208. @pytest.mark.asyncio
  209. async def test_func_str_res() -> None:
  210. def my_function(arg: str) -> str:
  211. return "test"
  212. tool = FunctionTool(my_function, description="Function tool.")
  213. result = await tool.run_json({"arg": "test"}, CancellationToken())
  214. assert tool.return_value_as_string(result) == "test"
  215. @pytest.mark.asyncio
  216. async def test_func_base_model_res() -> None:
  217. def my_function(arg: str) -> MyResult:
  218. return MyResult(result="test")
  219. tool = FunctionTool(my_function, description="Function tool.")
  220. result = await tool.run_json({"arg": "test"}, CancellationToken())
  221. assert tool.return_value_as_string(result) == '{"result": "test"}'
  222. @pytest.mark.asyncio
  223. async def test_func_base_model_custom_dump_res() -> None:
  224. class MyResultCustomDump(BaseModel):
  225. result: str = Field(description="The other description.")
  226. @model_serializer
  227. def ser_model(self) -> str:
  228. return "custom: " + self.result
  229. def my_function(arg: str) -> MyResultCustomDump:
  230. return MyResultCustomDump(result="test")
  231. tool = FunctionTool(my_function, description="Function tool.")
  232. result = await tool.run_json({"arg": "test"}, CancellationToken())
  233. assert tool.return_value_as_string(result) == "custom: test"
  234. @pytest.mark.asyncio
  235. async def test_func_int_res() -> None:
  236. def my_function(arg: int) -> int:
  237. return arg
  238. tool = FunctionTool(my_function, description="Function tool.")
  239. result = await tool.run_json({"arg": 5}, CancellationToken())
  240. assert tool.return_value_as_string(result) == "5"
  241. @pytest.mark.asyncio
  242. async def test_func_tool_return_list() -> None:
  243. def my_function() -> List[int]:
  244. return [1, 2]
  245. tool = FunctionTool(my_function, description="Function tool.")
  246. result = await tool.run_json({}, CancellationToken())
  247. assert isinstance(result, list)
  248. assert result == [1, 2]
  249. assert tool.return_value_as_string(result) == "[1, 2]"
  250. def test_nested_tool_schema_generation() -> None:
  251. schema: ToolSchema = MyNestedTool().schema
  252. assert "description" in schema
  253. assert "parameters" in schema
  254. assert "type" in schema["parameters"]
  255. assert "arg" in schema["parameters"]["properties"]
  256. assert "type" in schema["parameters"]["properties"]["arg"]
  257. assert "title" in schema["parameters"]["properties"]["arg"]
  258. assert "properties" in schema["parameters"]["properties"]["arg"]
  259. assert "query" in schema["parameters"]["properties"]["arg"]["properties"]
  260. assert "type" in schema["parameters"]["properties"]["arg"]["properties"]["query"]
  261. assert "description" in schema["parameters"]["properties"]["arg"]["properties"]["query"]
  262. assert "required" in schema["parameters"]
  263. assert schema["description"] == "Description of test nested tool."
  264. assert schema["parameters"]["type"] == "object"
  265. assert schema["parameters"]["properties"]["arg"]["type"] == "object"
  266. assert schema["parameters"]["properties"]["arg"]["title"] == "MyArgs"
  267. assert schema["parameters"]["properties"]["arg"]["properties"]["query"]["type"] == "string"
  268. assert schema["parameters"]["properties"]["arg"]["properties"]["query"]["description"] == "The description."
  269. assert schema["parameters"]["properties"]["arg"]["required"] == ["query"]
  270. assert schema["parameters"]["required"] == ["arg"]
  271. assert len(schema["parameters"]["properties"]) == 1
  272. @pytest.mark.asyncio
  273. async def test_nested_tool_run() -> None:
  274. tool = MyNestedTool()
  275. result = await tool.run_json({"arg": {"query": "test"}}, CancellationToken())
  276. assert isinstance(result, MyResult)
  277. assert result.result == "value"
  278. assert tool.called_count == 1
  279. result = await tool.run_json({"arg": {"query": "test"}}, CancellationToken())
  280. result = await tool.run_json({"arg": {"query": "test"}}, CancellationToken())
  281. assert tool.called_count == 3
  282. def test_nested_tool_properties() -> None:
  283. tool = MyNestedTool()
  284. assert tool.name == "TestNestedTool"
  285. assert tool.description == "Description of test nested tool."
  286. assert tool.args_type() == MyNestedArgs
  287. assert tool.return_type() == MyResult
  288. assert tool.state_type() is None