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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  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_strict() -> None:
  75. def my_function1(arg: str, other: Annotated[int, "int arg"], nonrequired: int = 5) -> MyResult:
  76. return MyResult(result="test")
  77. with pytest.raises(ValueError, match="Strict mode is enabled"):
  78. tool = FunctionTool(my_function1, description="Function tool.", strict=True)
  79. schema = tool.schema
  80. def my_function2(arg: str, other: Annotated[int, "int arg"]) -> MyResult:
  81. return MyResult(result="test")
  82. tool = FunctionTool(my_function2, description="Function tool.", strict=True)
  83. schema = tool.schema
  84. assert schema["name"] == "my_function2"
  85. assert "description" in schema
  86. assert schema["description"] == "Function tool."
  87. assert "parameters" in schema
  88. assert schema["parameters"]["type"] == "object"
  89. assert schema["parameters"]["properties"].keys() == {"arg", "other"}
  90. assert schema["parameters"]["properties"]["arg"]["type"] == "string"
  91. assert schema["parameters"]["properties"]["arg"]["description"] == "arg"
  92. assert schema["parameters"]["properties"]["other"]["type"] == "integer"
  93. assert schema["parameters"]["properties"]["other"]["description"] == "int arg"
  94. assert "required" in schema["parameters"]
  95. assert schema["parameters"]["required"] == ["arg", "other"]
  96. assert len(schema["parameters"]["properties"]) == 2
  97. assert "additionalProperties" in schema["parameters"]
  98. assert schema["parameters"]["additionalProperties"] is False
  99. def test_func_tool_schema_generation_only_default_arg() -> None:
  100. def my_function(arg: str = "default") -> MyResult:
  101. return MyResult(result="test")
  102. tool = FunctionTool(my_function, description="Function tool.")
  103. schema = tool.schema
  104. assert schema["name"] == "my_function"
  105. assert "description" in schema
  106. assert schema["description"] == "Function tool."
  107. assert "parameters" in schema
  108. assert len(schema["parameters"]["properties"]) == 1
  109. assert schema["parameters"]["properties"]["arg"]["type"] == "string"
  110. assert schema["parameters"]["properties"]["arg"]["description"] == "arg"
  111. assert "required" in schema["parameters"]
  112. assert schema["parameters"]["required"] == []
  113. def test_func_tool_schema_generation_only_default_arg_strict() -> None:
  114. def my_function(arg: str = "default") -> MyResult:
  115. return MyResult(result="test")
  116. with pytest.raises(ValueError, match="Strict mode is enabled"):
  117. tool = FunctionTool(my_function, description="Function tool.", strict=True)
  118. _ = tool.schema
  119. def test_func_tool_with_partial_positional_arguments_schema_generation() -> None:
  120. """Test correct schema generation for a partial function with positional arguments."""
  121. def get_weather(country: str, city: str) -> str:
  122. return f"The temperature in {city}, {country} is 75°"
  123. partial_function = partial(get_weather, "Germany")
  124. tool = FunctionTool(partial_function, description="Partial function tool.")
  125. schema = tool.schema
  126. assert schema["name"] == "get_weather"
  127. assert "description" in schema
  128. assert schema["description"] == "Partial function tool."
  129. assert "parameters" in schema
  130. assert schema["parameters"]["type"] == "object"
  131. assert schema["parameters"]["properties"].keys() == {"city"}
  132. assert schema["parameters"]["properties"]["city"]["type"] == "string"
  133. assert schema["parameters"]["properties"]["city"]["description"] == "city"
  134. assert "required" in schema["parameters"]
  135. assert schema["parameters"]["required"] == ["city"]
  136. assert "country" not in schema["parameters"]["properties"] # check country not in schema params
  137. assert len(schema["parameters"]["properties"]) == 1
  138. def test_func_call_tool_with_kwargs_schema_generation() -> None:
  139. """Test correct schema generation for a partial function with kwargs."""
  140. def get_weather(country: str, city: str) -> str:
  141. return f"The temperature in {city}, {country} is 75°"
  142. partial_function = partial(get_weather, country="Germany")
  143. tool = FunctionTool(partial_function, description="Partial function tool.")
  144. schema = tool.schema
  145. assert schema["name"] == "get_weather"
  146. assert "description" in schema
  147. assert schema["description"] == "Partial function tool."
  148. assert "parameters" in schema
  149. assert schema["parameters"]["type"] == "object"
  150. assert schema["parameters"]["properties"].keys() == {"country", "city"}
  151. assert schema["parameters"]["properties"]["city"]["type"] == "string"
  152. assert schema["parameters"]["properties"]["country"]["type"] == "string"
  153. assert "required" in schema["parameters"]
  154. assert schema["parameters"]["required"] == ["city"] # only city is required
  155. assert len(schema["parameters"]["properties"]) == 2
  156. @pytest.mark.asyncio
  157. async def test_run_func_call_tool_with_kwargs_and_args() -> None:
  158. """Test run partial function with kwargs and args."""
  159. def get_weather(country: str, city: str, unit: str = "Celsius") -> str:
  160. return f"The temperature in {city}, {country} is 75° {unit}"
  161. partial_function = partial(get_weather, "Germany", unit="Fahrenheit")
  162. tool = FunctionTool(partial_function, description="Partial function tool.")
  163. result = await tool.run_json({"city": "Berlin"}, CancellationToken())
  164. assert isinstance(result, str)
  165. assert result == "The temperature in Berlin, Germany is 75° Fahrenheit"
  166. @pytest.mark.asyncio
  167. async def test_tool_run() -> None:
  168. tool = MyTool()
  169. result = await tool.run_json({"query": "test"}, CancellationToken())
  170. assert isinstance(result, MyResult)
  171. assert result.result == "value"
  172. assert tool.called_count == 1
  173. result = await tool.run_json({"query": "test"}, CancellationToken())
  174. result = await tool.run_json({"query": "test"}, CancellationToken())
  175. assert tool.called_count == 3
  176. def test_tool_properties() -> None:
  177. tool = MyTool()
  178. assert tool.name == "TestTool"
  179. assert tool.description == "Description of test tool."
  180. assert tool.args_type() == MyArgs
  181. assert tool.return_type() == MyResult
  182. assert tool.state_type() is None
  183. def test_get_typed_signature() -> None:
  184. def my_function() -> str:
  185. return "result"
  186. sig = get_typed_signature(my_function)
  187. assert isinstance(sig, inspect.Signature)
  188. assert len(sig.parameters) == 0
  189. assert sig.return_annotation is str
  190. def test_get_typed_signature_annotated() -> None:
  191. def my_function() -> Annotated[str, "The return type"]:
  192. return "result"
  193. sig = get_typed_signature(my_function)
  194. assert isinstance(sig, inspect.Signature)
  195. assert len(sig.parameters) == 0
  196. assert sig.return_annotation == Annotated[str, "The return type"]
  197. def test_get_typed_signature_string() -> None:
  198. def my_function() -> "str":
  199. return "result"
  200. sig = get_typed_signature(my_function)
  201. assert isinstance(sig, inspect.Signature)
  202. assert len(sig.parameters) == 0
  203. assert sig.return_annotation is str
  204. def test_get_typed_signature_params() -> None:
  205. def my_function(arg: str) -> None:
  206. return None
  207. sig = get_typed_signature(my_function)
  208. assert isinstance(sig, inspect.Signature)
  209. assert sig.return_annotation is type(None)
  210. assert len(sig.parameters) == 1
  211. assert sig.parameters["arg"].annotation is str
  212. def test_get_typed_signature_two_params() -> None:
  213. def my_function(arg: str, arg2: int) -> None:
  214. return None
  215. sig = get_typed_signature(my_function)
  216. assert isinstance(sig, inspect.Signature)
  217. assert len(sig.parameters) == 2
  218. assert sig.parameters["arg"].annotation is str
  219. assert sig.parameters["arg2"].annotation is int
  220. def test_get_typed_signature_param_str() -> None:
  221. def my_function(arg: "str") -> None:
  222. return None
  223. sig = get_typed_signature(my_function)
  224. assert isinstance(sig, inspect.Signature)
  225. assert len(sig.parameters) == 1
  226. assert sig.parameters["arg"].annotation is str
  227. def test_get_typed_signature_param_annotated() -> None:
  228. def my_function(arg: Annotated[str, "An arg"]) -> None:
  229. return None
  230. sig = get_typed_signature(my_function)
  231. assert isinstance(sig, inspect.Signature)
  232. assert len(sig.parameters) == 1
  233. assert sig.parameters["arg"].annotation == Annotated[str, "An arg"]
  234. def test_func_tool() -> None:
  235. def my_function() -> str:
  236. return "result"
  237. tool = FunctionTool(my_function, description="Function tool.")
  238. assert tool.name == "my_function"
  239. assert tool.description == "Function tool."
  240. assert issubclass(tool.args_type(), BaseModel)
  241. assert issubclass(tool.return_type(), str)
  242. assert tool.state_type() is None
  243. def test_func_tool_annotated_arg() -> None:
  244. def my_function(my_arg: Annotated[str, "test description"]) -> str:
  245. return "result"
  246. tool = FunctionTool(my_function, description="Function tool.")
  247. assert tool.name == "my_function"
  248. assert tool.description == "Function tool."
  249. assert issubclass(tool.args_type(), BaseModel)
  250. assert issubclass(tool.return_type(), str)
  251. assert tool.args_type().model_fields["my_arg"].description == "test description"
  252. assert tool.args_type().model_fields["my_arg"].annotation is str
  253. assert tool.args_type().model_fields["my_arg"].is_required() is True
  254. assert tool.args_type().model_fields["my_arg"].default is PydanticUndefined
  255. assert len(tool.args_type().model_fields) == 1
  256. assert tool.return_type() is str
  257. assert tool.state_type() is None
  258. def test_func_tool_return_annotated() -> None:
  259. def my_function() -> Annotated[str, "test description"]:
  260. return "result"
  261. tool = FunctionTool(my_function, description="Function tool.")
  262. assert tool.name == "my_function"
  263. assert tool.description == "Function tool."
  264. assert issubclass(tool.args_type(), BaseModel)
  265. assert tool.return_type() is str
  266. assert tool.state_type() is None
  267. def test_func_tool_no_args() -> None:
  268. def my_function() -> str:
  269. return "result"
  270. tool = FunctionTool(my_function, description="Function tool.")
  271. assert tool.name == "my_function"
  272. assert tool.description == "Function tool."
  273. assert issubclass(tool.args_type(), BaseModel)
  274. assert len(tool.args_type().model_fields) == 0
  275. assert tool.return_type() is str
  276. assert tool.state_type() is None
  277. def test_func_tool_return_none() -> None:
  278. def my_function() -> None:
  279. return None
  280. tool = FunctionTool(my_function, description="Function tool.")
  281. assert tool.name == "my_function"
  282. assert tool.description == "Function tool."
  283. assert issubclass(tool.args_type(), BaseModel)
  284. assert tool.return_type() is type(None)
  285. assert tool.state_type() is None
  286. def test_func_tool_return_base_model() -> None:
  287. def my_function() -> MyResult:
  288. return MyResult(result="value")
  289. tool = FunctionTool(my_function, description="Function tool.")
  290. assert tool.name == "my_function"
  291. assert tool.description == "Function tool."
  292. assert issubclass(tool.args_type(), BaseModel)
  293. assert tool.return_type() is MyResult
  294. assert tool.state_type() is None
  295. @pytest.mark.asyncio
  296. async def test_func_call_tool() -> None:
  297. def my_function() -> str:
  298. return "result"
  299. tool = FunctionTool(my_function, description="Function tool.")
  300. result = await tool.run_json({}, CancellationToken())
  301. assert result == "result"
  302. @pytest.mark.asyncio
  303. async def test_func_call_tool_base_model() -> None:
  304. def my_function() -> MyResult:
  305. return MyResult(result="value")
  306. tool = FunctionTool(my_function, description="Function tool.")
  307. result = await tool.run_json({}, CancellationToken())
  308. assert isinstance(result, MyResult)
  309. assert result.result == "value"
  310. @pytest.mark.asyncio
  311. async def test_func_call_tool_with_arg_base_model() -> None:
  312. def my_function(arg: str) -> MyResult:
  313. return MyResult(result="value")
  314. tool = FunctionTool(my_function, description="Function tool.")
  315. result = await tool.run_json({"arg": "test"}, CancellationToken())
  316. assert isinstance(result, MyResult)
  317. assert result.result == "value"
  318. @pytest.mark.asyncio
  319. async def test_func_str_res() -> None:
  320. def my_function(arg: str) -> str:
  321. return "test"
  322. tool = FunctionTool(my_function, description="Function tool.")
  323. result = await tool.run_json({"arg": "test"}, CancellationToken())
  324. assert tool.return_value_as_string(result) == "test"
  325. @pytest.mark.asyncio
  326. async def test_func_base_model_res() -> None:
  327. def my_function(arg: str) -> MyResult:
  328. return MyResult(result="test")
  329. tool = FunctionTool(my_function, description="Function tool.")
  330. result = await tool.run_json({"arg": "test"}, CancellationToken())
  331. assert tool.return_value_as_string(result) == '{"result": "test"}'
  332. @pytest.mark.asyncio
  333. async def test_func_base_model_custom_dump_res() -> None:
  334. class MyResultCustomDump(BaseModel):
  335. result: str = Field(description="The other description.")
  336. @model_serializer
  337. def ser_model(self) -> str:
  338. return "custom: " + self.result
  339. def my_function(arg: str) -> MyResultCustomDump:
  340. return MyResultCustomDump(result="test")
  341. tool = FunctionTool(my_function, description="Function tool.")
  342. result = await tool.run_json({"arg": "test"}, CancellationToken())
  343. assert tool.return_value_as_string(result) == "custom: test"
  344. @pytest.mark.asyncio
  345. async def test_func_int_res() -> None:
  346. def my_function(arg: int) -> int:
  347. return arg
  348. tool = FunctionTool(my_function, description="Function tool.")
  349. result = await tool.run_json({"arg": 5}, CancellationToken())
  350. assert tool.return_value_as_string(result) == "5"
  351. @pytest.mark.asyncio
  352. async def test_func_tool_return_list() -> None:
  353. def my_function() -> List[int]:
  354. return [1, 2]
  355. tool = FunctionTool(my_function, description="Function tool.")
  356. result = await tool.run_json({}, CancellationToken())
  357. assert isinstance(result, list)
  358. assert result == [1, 2]
  359. assert tool.return_value_as_string(result) == "[1, 2]"
  360. def test_nested_tool_schema_generation() -> None:
  361. schema: ToolSchema = MyNestedTool().schema
  362. assert "description" in schema
  363. assert "parameters" in schema
  364. assert "type" in schema["parameters"]
  365. assert "arg" in schema["parameters"]["properties"]
  366. assert "type" in schema["parameters"]["properties"]["arg"]
  367. assert "title" in schema["parameters"]["properties"]["arg"]
  368. assert "properties" in schema["parameters"]["properties"]["arg"]
  369. assert "query" in schema["parameters"]["properties"]["arg"]["properties"]
  370. assert "type" in schema["parameters"]["properties"]["arg"]["properties"]["query"]
  371. assert "description" in schema["parameters"]["properties"]["arg"]["properties"]["query"]
  372. assert "required" in schema["parameters"]
  373. assert schema["description"] == "Description of test nested tool."
  374. assert schema["parameters"]["type"] == "object"
  375. assert schema["parameters"]["properties"]["arg"]["type"] == "object"
  376. assert schema["parameters"]["properties"]["arg"]["title"] == "MyArgs"
  377. assert schema["parameters"]["properties"]["arg"]["properties"]["query"]["type"] == "string"
  378. assert schema["parameters"]["properties"]["arg"]["properties"]["query"]["description"] == "The description."
  379. assert schema["parameters"]["properties"]["arg"]["required"] == ["query"]
  380. assert schema["parameters"]["required"] == ["arg"]
  381. assert len(schema["parameters"]["properties"]) == 1
  382. @pytest.mark.asyncio
  383. async def test_nested_tool_run() -> None:
  384. tool = MyNestedTool()
  385. result = await tool.run_json({"arg": {"query": "test"}}, CancellationToken())
  386. assert isinstance(result, MyResult)
  387. assert result.result == "value"
  388. assert tool.called_count == 1
  389. result = await tool.run_json({"arg": {"query": "test"}}, CancellationToken())
  390. result = await tool.run_json({"arg": {"query": "test"}}, CancellationToken())
  391. assert tool.called_count == 3
  392. def test_nested_tool_properties() -> None:
  393. tool = MyNestedTool()
  394. assert tool.name == "TestNestedTool"
  395. assert tool.description == "Description of test nested tool."
  396. assert tool.args_type() == MyNestedArgs
  397. assert tool.return_type() == MyResult
  398. assert tool.state_type() is None