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_function_utils.py 13 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. import inspect
  2. import unittest.mock
  3. from typing import Dict, List, Literal, Optional, Tuple
  4. import pytest
  5. from pydantic import BaseModel, Field
  6. from typing_extensions import Annotated
  7. from autogen._pydantic import PYDANTIC_V1, model_dump
  8. from autogen.function_utils import (
  9. get_default_values,
  10. get_function_schema,
  11. get_load_param_if_needed_function,
  12. get_missing_annotations,
  13. get_param_annotations,
  14. get_parameter_json_schema,
  15. get_parameters,
  16. get_required_params,
  17. get_typed_annotation,
  18. get_typed_return_annotation,
  19. get_typed_signature,
  20. load_basemodels_if_needed,
  21. serialize_to_str,
  22. )
  23. def f(a: Annotated[str, "Parameter a"], b: int = 2, c: Annotated[float, "Parameter c"] = 0.1, *, d):
  24. pass
  25. def g(
  26. a: Annotated[str, "Parameter a"],
  27. b: int = 2,
  28. c: Annotated[float, "Parameter c"] = 0.1,
  29. *,
  30. d: Dict[str, Tuple[Optional[int], List[float]]],
  31. ) -> str:
  32. pass
  33. async def a_g(
  34. a: Annotated[str, "Parameter a"],
  35. b: int = 2,
  36. c: Annotated[float, "Parameter c"] = 0.1,
  37. *,
  38. d: Dict[str, Tuple[Optional[int], List[float]]],
  39. ) -> str:
  40. pass
  41. def test_get_typed_annotation() -> None:
  42. globalns = getattr(f, "__globals__", {})
  43. assert get_typed_annotation(str, globalns) == str
  44. assert get_typed_annotation("float", globalns) == float
  45. def test_get_typed_signature() -> None:
  46. assert get_typed_signature(f).parameters == inspect.signature(f).parameters
  47. assert get_typed_signature(g).parameters == inspect.signature(g).parameters
  48. def test_get_typed_return_annotation() -> None:
  49. assert get_typed_return_annotation(f) is None
  50. assert get_typed_return_annotation(g) == str
  51. def test_get_parameter_json_schema() -> None:
  52. assert get_parameter_json_schema("c", str, {}) == {"type": "string", "description": "c"}
  53. assert get_parameter_json_schema("c", str, {"c": "ccc"}) == {"type": "string", "description": "c", "default": "ccc"}
  54. assert get_parameter_json_schema("a", Annotated[str, "parameter a"], {}) == {
  55. "type": "string",
  56. "description": "parameter a",
  57. }
  58. assert get_parameter_json_schema("a", Annotated[str, "parameter a"], {"a": "3.14"}) == {
  59. "type": "string",
  60. "description": "parameter a",
  61. "default": "3.14",
  62. }
  63. class B(BaseModel):
  64. b: float
  65. c: str
  66. expected = {
  67. "description": "b",
  68. "properties": {"b": {"title": "B", "type": "number"}, "c": {"title": "C", "type": "string"}},
  69. "required": ["b", "c"],
  70. "title": "B",
  71. "type": "object",
  72. }
  73. assert get_parameter_json_schema("b", B, {}) == expected
  74. expected["default"] = B(b=1.2, c="3.4")
  75. assert get_parameter_json_schema("b", B, {"b": B(b=1.2, c="3.4")}) == expected
  76. def test_get_required_params() -> None:
  77. assert get_required_params(inspect.signature(f)) == ["a", "d"]
  78. assert get_required_params(inspect.signature(g)) == ["a", "d"]
  79. def test_get_default_values() -> None:
  80. assert get_default_values(inspect.signature(f)) == {"b": 2, "c": 0.1}
  81. assert get_default_values(inspect.signature(g)) == {"b": 2, "c": 0.1}
  82. def test_get_param_annotations() -> None:
  83. def f(a: Annotated[str, "Parameter a"], b=1, c: Annotated[float, "Parameter c"] = 1.0):
  84. pass
  85. expected = {"a": Annotated[str, "Parameter a"], "c": Annotated[float, "Parameter c"]}
  86. typed_signature = get_typed_signature(f)
  87. param_annotations = get_param_annotations(typed_signature)
  88. assert param_annotations == expected, param_annotations
  89. def test_get_missing_annotations() -> None:
  90. def _f1(a: str, b=2):
  91. pass
  92. missing, unannotated_with_default = get_missing_annotations(get_typed_signature(_f1), ["a"])
  93. assert missing == set()
  94. assert unannotated_with_default == {"b"}
  95. def _f2(a: str, b) -> str:
  96. "ok"
  97. missing, unannotated_with_default = get_missing_annotations(get_typed_signature(_f2), ["a", "b"])
  98. assert missing == {"b"}
  99. assert unannotated_with_default == set()
  100. def _f3() -> None:
  101. pass
  102. missing, unannotated_with_default = get_missing_annotations(get_typed_signature(_f3), [])
  103. assert missing == set()
  104. assert unannotated_with_default == set()
  105. def test_get_parameters() -> None:
  106. def f(a: Annotated[str, "Parameter a"], b=1, c: Annotated[float, "Parameter c"] = 1.0):
  107. pass
  108. typed_signature = get_typed_signature(f)
  109. param_annotations = get_param_annotations(typed_signature)
  110. required = get_required_params(typed_signature)
  111. default_values = get_default_values(typed_signature)
  112. expected = {
  113. "type": "object",
  114. "properties": {
  115. "a": {"type": "string", "description": "Parameter a"},
  116. "c": {"type": "number", "description": "Parameter c", "default": 1.0},
  117. },
  118. "required": ["a"],
  119. }
  120. actual = model_dump(get_parameters(required, param_annotations, default_values))
  121. assert actual == expected, actual
  122. def test_get_function_schema_no_return_type() -> None:
  123. def f(a: Annotated[str, "Parameter a"], b: int, c: float = 0.1):
  124. pass
  125. expected = (
  126. "The return type of the function 'f' is not annotated. Although annotating it is "
  127. + "optional, the function should return either a string, a subclass of 'pydantic.BaseModel'."
  128. )
  129. with unittest.mock.patch("autogen.function_utils.logger.warning") as mock_logger_warning:
  130. get_function_schema(f, description="function g")
  131. mock_logger_warning.assert_called_once_with(expected)
  132. def test_get_function_schema_unannotated_with_default() -> None:
  133. with unittest.mock.patch("autogen.function_utils.logger.warning") as mock_logger_warning:
  134. def f(
  135. a: Annotated[str, "Parameter a"], b=2, c: Annotated[float, "Parameter c"] = 0.1, d="whatever", e=None
  136. ) -> str:
  137. return "ok"
  138. get_function_schema(f, description="function f")
  139. mock_logger_warning.assert_called_once_with(
  140. "The following parameters of the function 'f' with default values are not annotated: 'b', 'd', 'e'."
  141. )
  142. def test_get_function_schema_missing() -> None:
  143. def f(a: Annotated[str, "Parameter a"], b, c: Annotated[float, "Parameter c"] = 0.1) -> float:
  144. pass
  145. expected = (
  146. "All parameters of the function 'f' without default values must be annotated. "
  147. + "The annotations are missing for the following parameters: 'b'"
  148. )
  149. with pytest.raises(TypeError) as e:
  150. get_function_schema(f, description="function f")
  151. assert str(e.value) == expected, e.value
  152. def test_get_function_schema() -> None:
  153. expected_v2 = {
  154. "description": "function g",
  155. "name": "fancy name for g",
  156. "parameters": {
  157. "type": "object",
  158. "properties": {
  159. "a": {"type": "string", "description": "Parameter a"},
  160. "b": {"type": "integer", "description": "b", "default": 2},
  161. "c": {"type": "number", "description": "Parameter c", "default": 0.1},
  162. "d": {
  163. "additionalProperties": {
  164. "maxItems": 2,
  165. "minItems": 2,
  166. "prefixItems": [
  167. {"anyOf": [{"type": "integer"}, {"type": "null"}]},
  168. {"items": {"type": "number"}, "type": "array"},
  169. ],
  170. "type": "array",
  171. },
  172. "type": "object",
  173. "description": "d",
  174. },
  175. },
  176. "required": ["a", "d"],
  177. },
  178. }
  179. # the difference is that the v1 version does not handle Union types (Optional is Union[T, None])
  180. expected_v1 = {
  181. "description": "function g",
  182. "name": "fancy name for g",
  183. "parameters": {
  184. "type": "object",
  185. "properties": {
  186. "a": {"type": "string", "description": "Parameter a"},
  187. "b": {"type": "integer", "description": "b", "default": 2},
  188. "c": {"type": "number", "description": "Parameter c", "default": 0.1},
  189. "d": {
  190. "type": "object",
  191. "additionalProperties": {
  192. "type": "array",
  193. "minItems": 2,
  194. "maxItems": 2,
  195. "items": [{"type": "integer"}, {"type": "array", "items": {"type": "number"}}],
  196. },
  197. "description": "d",
  198. },
  199. },
  200. "required": ["a", "d"],
  201. },
  202. }
  203. actual = get_function_schema(g, description="function g", name="fancy name for g")
  204. if PYDANTIC_V1:
  205. assert actual == expected_v1, actual
  206. else:
  207. assert actual == expected_v2, actual
  208. actual = get_function_schema(a_g, description="function g", name="fancy name for g")
  209. if PYDANTIC_V1:
  210. assert actual == expected_v1, actual
  211. else:
  212. assert actual == expected_v2, actual
  213. CurrencySymbol = Literal["USD", "EUR"]
  214. class Currency(BaseModel):
  215. currency: Annotated[CurrencySymbol, Field(..., description="Currency code")]
  216. amount: Annotated[float, Field(100.0, description="Amount of money in the currency")]
  217. def test_get_function_schema_pydantic() -> None:
  218. def currency_calculator(
  219. base: Annotated[Currency, "Base currency: amount and currency symbol"],
  220. quote_currency: Annotated[CurrencySymbol, "Quote currency symbol (default: 'EUR')"] = "EUR",
  221. ) -> Currency:
  222. pass
  223. expected = {
  224. "description": "Currency exchange calculator.",
  225. "name": "currency_calculator",
  226. "parameters": {
  227. "type": "object",
  228. "properties": {
  229. "base": {
  230. "properties": {
  231. "currency": {
  232. "description": "Currency code",
  233. "enum": ["USD", "EUR"],
  234. "title": "Currency",
  235. "type": "string",
  236. },
  237. "amount": {
  238. "default": 100.0,
  239. "description": "Amount of money in the currency",
  240. "title": "Amount",
  241. "type": "number",
  242. },
  243. },
  244. "required": ["currency"],
  245. "title": "Currency",
  246. "type": "object",
  247. "description": "Base currency: amount and currency symbol",
  248. },
  249. "quote_currency": {
  250. "enum": ["USD", "EUR"],
  251. "type": "string",
  252. "default": "EUR",
  253. "description": "Quote currency symbol (default: 'EUR')",
  254. },
  255. },
  256. "required": ["base"],
  257. },
  258. }
  259. actual = get_function_schema(
  260. currency_calculator, description="Currency exchange calculator.", name="currency_calculator"
  261. )
  262. assert actual == expected, actual
  263. def test_get_load_param_if_needed_function() -> None:
  264. assert get_load_param_if_needed_function(CurrencySymbol) is None
  265. assert get_load_param_if_needed_function(Currency)({"currency": "USD", "amount": 123.45}, Currency) == Currency(
  266. currency="USD", amount=123.45
  267. )
  268. f = get_load_param_if_needed_function(Annotated[Currency, "amount and a symbol of a currency"])
  269. actual = f({"currency": "USD", "amount": 123.45}, Currency)
  270. expected = Currency(currency="USD", amount=123.45)
  271. assert actual == expected, actual
  272. def test_load_basemodels_if_needed() -> None:
  273. @load_basemodels_if_needed
  274. def f(
  275. base: Annotated[Currency, "Base currency"],
  276. quote_currency: Annotated[CurrencySymbol, "Quote currency"] = "EUR",
  277. ) -> Tuple[Currency, CurrencySymbol]:
  278. return base, quote_currency
  279. actual = f(base={"currency": "USD", "amount": 123.45}, quote_currency="EUR")
  280. assert isinstance(actual[0], Currency)
  281. assert actual[0].amount == 123.45
  282. assert actual[0].currency == "USD"
  283. assert actual[1] == "EUR"
  284. def test_serialize_to_json():
  285. assert serialize_to_str("abc") == "abc"
  286. assert serialize_to_str(123) == "123"
  287. assert serialize_to_str([123, 456]) == "[123, 456]"
  288. assert serialize_to_str({"a": 1, "b": 2.3}) == '{"a": 1, "b": 2.3}'
  289. class A(BaseModel):
  290. a: int
  291. b: float
  292. c: str
  293. assert serialize_to_str(A(a=1, b=2.3, c="abc")) == '{"a":1,"b":2.3,"c":"abc"}'