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

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