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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. import inspect
  2. from typing import Annotated
  3. import pytest
  4. from agnext.components._function_utils import get_typed_signature
  5. from agnext.components.tools import BaseTool, FunctionTool
  6. from agnext.components.models._openai_client import convert_tools
  7. from agnext.core import CancellationToken
  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 MyResult(BaseModel):
  13. result: str = Field(description="The other description.")
  14. class MyTool(BaseTool[MyArgs, MyResult]):
  15. def __init__(self) -> None:
  16. super().__init__(
  17. args_type=MyArgs,
  18. return_type=MyResult,
  19. name="TestTool",
  20. description="Description of test tool.",
  21. )
  22. self.called_count = 0
  23. async def run(self, args: MyArgs, cancellation_token: CancellationToken) -> MyResult:
  24. self.called_count += 1
  25. return MyResult(result="value")
  26. def test_tool_schema_generation() -> None:
  27. schema = MyTool().schema
  28. assert schema["name"] == "TestTool"
  29. assert "description" in schema
  30. assert schema["description"] == "Description of test tool."
  31. assert "parameters" in schema
  32. assert schema["parameters"]["type"] == "object"
  33. assert "properties" in schema["parameters"]
  34. assert schema["parameters"]["properties"]["query"]["description"] == "The description."
  35. assert schema["parameters"]["properties"]["query"]["type"] == "string"
  36. assert "required" in schema["parameters"]
  37. assert schema["parameters"]["required"] == ["query"]
  38. assert len(schema["parameters"]["properties"]) == 1
  39. def test_func_tool_schema_generation() -> None:
  40. def my_function(arg: str, other: Annotated[int, "int arg"], nonrequired: int = 5) -> MyResult:
  41. return MyResult(result="test")
  42. tool = FunctionTool(my_function, description="Function tool.")
  43. schema = tool.schema
  44. assert schema["name"] == "my_function"
  45. assert "description" in schema
  46. assert schema["description"] == "Function tool."
  47. assert "parameters" in schema
  48. assert schema["parameters"]["type"] == "object"
  49. assert schema["parameters"]["properties"].keys() == {"arg", "other", "nonrequired"}
  50. assert schema["parameters"]["properties"]["arg"]["type"] == "string"
  51. assert schema["parameters"]["properties"]["arg"]["description"] == "arg"
  52. assert schema["parameters"]["properties"]["other"]["type"] == "integer"
  53. assert schema["parameters"]["properties"]["other"]["description"] == "int arg"
  54. assert schema["parameters"]["properties"]["nonrequired"]["type"] == "integer"
  55. assert schema["parameters"]["properties"]["nonrequired"]["description"] == "nonrequired"
  56. assert "required" in schema["parameters"]
  57. assert schema["parameters"]["required"] == ["arg", "other"]
  58. assert len(schema["parameters"]["properties"]) == 3
  59. def test_func_tool_schema_generation_only_default_arg() -> None:
  60. def my_function(arg: str = "default") -> MyResult:
  61. return MyResult(result="test")
  62. tool = FunctionTool(my_function, description="Function tool.")
  63. schema = tool.schema
  64. assert schema["name"] == "my_function"
  65. assert "description" in schema
  66. assert schema["description"] == "Function tool."
  67. assert "parameters" in schema
  68. assert len(schema["parameters"]["properties"]) == 1
  69. assert schema["parameters"]["properties"]["arg"]["type"] == "string"
  70. assert schema["parameters"]["properties"]["arg"]["description"] == "arg"
  71. assert "required" not in schema["parameters"]
  72. @pytest.mark.asyncio
  73. async def test_tool_run()-> None:
  74. tool = MyTool()
  75. result = await tool.run_json({"query": "test"}, CancellationToken())
  76. assert isinstance(result, MyResult)
  77. assert result.result == "value"
  78. assert tool.called_count == 1
  79. result = await tool.run_json({"query": "test"}, CancellationToken())
  80. result = await tool.run_json({"query": "test"}, CancellationToken())
  81. assert tool.called_count == 3
  82. def test_tool_properties()-> None:
  83. tool = MyTool()
  84. assert tool.name == "TestTool"
  85. assert tool.description == "Description of test tool."
  86. assert tool.args_type() == MyArgs
  87. assert tool.return_type() == MyResult
  88. assert tool.state_type() is None
  89. def test_get_typed_signature()-> None:
  90. def my_function() -> str:
  91. return "result"
  92. sig = get_typed_signature(my_function)
  93. assert isinstance(sig, inspect.Signature)
  94. assert len(sig.parameters) == 0
  95. assert sig.return_annotation == str
  96. def test_get_typed_signature_annotated()-> None:
  97. def my_function() -> Annotated[str, "The return type"]:
  98. return "result"
  99. sig = get_typed_signature(my_function)
  100. assert isinstance(sig, inspect.Signature)
  101. assert len(sig.parameters) == 0
  102. assert sig.return_annotation == Annotated[str, "The return type"]
  103. def test_get_typed_signature_string()-> 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_func_tool()-> None:
  111. def my_function() -> str:
  112. return "result"
  113. tool = FunctionTool(my_function, description="Function tool.")
  114. assert tool.name == "my_function"
  115. assert tool.description == "Function tool."
  116. assert issubclass(tool.args_type(), BaseModel)
  117. assert issubclass(tool.return_type(), str)
  118. assert tool.state_type() is None
  119. def test_func_tool_annotated_arg()-> None:
  120. def my_function(my_arg: Annotated[str, "test description"]) -> str:
  121. return "result"
  122. tool = FunctionTool(my_function, description="Function tool.")
  123. assert tool.name == "my_function"
  124. assert tool.description == "Function tool."
  125. assert issubclass(tool.args_type(), BaseModel)
  126. assert issubclass(tool.return_type(), str)
  127. assert tool.args_type().model_fields["my_arg"].description == "test description"
  128. assert tool.args_type().model_fields["my_arg"].annotation == str
  129. assert tool.args_type().model_fields["my_arg"].is_required() is True
  130. assert tool.args_type().model_fields["my_arg"].default is PydanticUndefined
  131. assert len(tool.args_type().model_fields) == 1
  132. assert tool.return_type() == str
  133. assert tool.state_type() is None
  134. def test_func_tool_return_annotated()-> None:
  135. def my_function() -> Annotated[str, "test description"]:
  136. return "result"
  137. tool = FunctionTool(my_function, description="Function tool.")
  138. assert tool.name == "my_function"
  139. assert tool.description == "Function tool."
  140. assert issubclass(tool.args_type(), BaseModel)
  141. assert tool.return_type() == str
  142. assert tool.state_type() is None
  143. def test_func_tool_no_args()-> None:
  144. def my_function() -> str:
  145. return "result"
  146. tool = FunctionTool(my_function, description="Function tool.")
  147. assert tool.name == "my_function"
  148. assert tool.description == "Function tool."
  149. assert issubclass(tool.args_type(), BaseModel)
  150. assert len(tool.args_type().model_fields) == 0
  151. assert tool.return_type() == str
  152. assert tool.state_type() is None
  153. def test_func_tool_return_none()-> None:
  154. def my_function() -> None:
  155. return None
  156. tool = FunctionTool(my_function, description="Function tool.")
  157. assert tool.name == "my_function"
  158. assert tool.description == "Function tool."
  159. assert issubclass(tool.args_type(), BaseModel)
  160. assert tool.return_type() is None
  161. assert tool.state_type() is None
  162. def test_func_tool_return_base_model()-> None:
  163. def my_function() -> MyResult:
  164. return MyResult(result="value")
  165. tool = FunctionTool(my_function, description="Function tool.")
  166. assert tool.name == "my_function"
  167. assert tool.description == "Function tool."
  168. assert issubclass(tool.args_type(), BaseModel)
  169. assert tool.return_type() is MyResult
  170. assert tool.state_type() is None
  171. @pytest.mark.asyncio
  172. async def test_func_call_tool()-> None:
  173. def my_function() -> str:
  174. return "result"
  175. tool = FunctionTool(my_function, description="Function tool.")
  176. result = await tool.run_json({}, CancellationToken())
  177. assert result == "result"
  178. @pytest.mark.asyncio
  179. async def test_func_call_tool_base_model()-> None:
  180. def my_function() -> MyResult:
  181. return MyResult(result="value")
  182. tool = FunctionTool(my_function, description="Function tool.")
  183. result = await tool.run_json({}, CancellationToken())
  184. assert isinstance(result, MyResult)
  185. assert result.result == "value"
  186. @pytest.mark.asyncio
  187. async def test_func_call_tool_with_arg_base_model()-> None:
  188. def my_function(arg: str) -> MyResult:
  189. return MyResult(result="value")
  190. tool = FunctionTool(my_function, description="Function tool.")
  191. result = await tool.run_json({"arg": "test"}, CancellationToken())
  192. assert isinstance(result, MyResult)
  193. assert result.result == "value"
  194. @pytest.mark.asyncio
  195. async def test_func_str_res()-> None:
  196. def my_function(arg: str) -> str:
  197. return "test"
  198. tool = FunctionTool(my_function, description="Function tool.")
  199. result = await tool.run_json({"arg": "test"}, CancellationToken())
  200. assert tool.return_value_as_string(result) == "test"
  201. @pytest.mark.asyncio
  202. async def test_func_base_model_res()-> None:
  203. def my_function(arg: str) -> MyResult:
  204. return MyResult(result="test")
  205. tool = FunctionTool(my_function, description="Function tool.")
  206. result = await tool.run_json({"arg": "test"}, CancellationToken())
  207. assert tool.return_value_as_string(result) == '{"result": "test"}'
  208. @pytest.mark.asyncio
  209. async def test_func_base_model_custom_dump_res()-> None:
  210. class MyResultCustomDump(BaseModel):
  211. result: str = Field(description="The other description.")
  212. @model_serializer
  213. def ser_model(self) -> str:
  214. return "custom: " + self.result
  215. def my_function(arg: str) -> MyResultCustomDump:
  216. return MyResultCustomDump(result="test")
  217. tool = FunctionTool(my_function, description="Function tool.")
  218. result = await tool.run_json({"arg": "test"}, CancellationToken())
  219. assert tool.return_value_as_string(result) == "custom: test"
  220. @pytest.mark.asyncio
  221. async def test_func_int_res()-> None:
  222. def my_function(arg: int) -> int:
  223. return arg
  224. tool = FunctionTool(my_function, description="Function tool.")
  225. result = await tool.run_json({"arg": 5}, CancellationToken())
  226. assert tool.return_value_as_string(result) == "5"
  227. def test_convert_tools_accepts_both_func_tool_and_schema() -> None:
  228. def my_function(arg: str, other: Annotated[int, "int arg"], nonrequired: int = 5) -> MyResult:
  229. return MyResult(result="test")
  230. tool = FunctionTool(my_function, description="Function tool.")
  231. schema = tool.schema
  232. converted_tool_schema = convert_tools([tool, schema])
  233. assert len(converted_tool_schema) == 2
  234. assert converted_tool_schema[0] == converted_tool_schema[1]
  235. def test_convert_tools_accepts_both_tool_and_schema() -> None:
  236. tool = MyTool()
  237. schema = tool.schema
  238. converted_tool_schema = convert_tools([tool, schema])
  239. assert len(converted_tool_schema) == 2
  240. assert converted_tool_schema[0] == converted_tool_schema[1]