@ixo/common 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. package/.eslintrc.js +9 -0
  2. package/.prettierignore +3 -0
  3. package/.prettierrc.cjs +4 -0
  4. package/.turbo/turbo-build.log +4 -0
  5. package/CHANGELOG.md +76 -0
  6. package/README.md +245 -0
  7. package/dist/ai/checkpointer/index.d.ts +2 -0
  8. package/dist/ai/checkpointer/index.d.ts.map +1 -0
  9. package/dist/ai/checkpointer/index.js +2 -0
  10. package/dist/ai/checkpointer/index.js.map +1 -0
  11. package/dist/ai/index.d.ts +9 -0
  12. package/dist/ai/index.d.ts.map +1 -0
  13. package/dist/ai/index.js +9 -0
  14. package/dist/ai/index.js.map +1 -0
  15. package/dist/ai/models/index.d.ts +2 -0
  16. package/dist/ai/models/index.d.ts.map +1 -0
  17. package/dist/ai/models/index.js +2 -0
  18. package/dist/ai/models/index.js.map +1 -0
  19. package/dist/ai/models/openai.d.ts +10 -0
  20. package/dist/ai/models/openai.d.ts.map +1 -0
  21. package/dist/ai/models/openai.js +38 -0
  22. package/dist/ai/models/openai.js.map +1 -0
  23. package/dist/ai/models/openai.test.d.ts +2 -0
  24. package/dist/ai/models/openai.test.d.ts.map +1 -0
  25. package/dist/ai/models/openai.test.js +58 -0
  26. package/dist/ai/models/openai.test.js.map +1 -0
  27. package/dist/ai/nodes/create-fake-node.d.ts +2 -0
  28. package/dist/ai/nodes/create-fake-node.d.ts.map +1 -0
  29. package/dist/ai/nodes/create-fake-node.js +2 -0
  30. package/dist/ai/nodes/create-fake-node.js.map +1 -0
  31. package/dist/ai/nodes/find-docs/find-docs.prompt.d.ts +3 -0
  32. package/dist/ai/nodes/find-docs/find-docs.prompt.d.ts.map +1 -0
  33. package/dist/ai/nodes/find-docs/find-docs.prompt.js +61 -0
  34. package/dist/ai/nodes/find-docs/find-docs.prompt.js.map +1 -0
  35. package/dist/ai/nodes/find-docs/index.d.ts +3 -0
  36. package/dist/ai/nodes/find-docs/index.d.ts.map +1 -0
  37. package/dist/ai/nodes/find-docs/index.js +3 -0
  38. package/dist/ai/nodes/find-docs/index.js.map +1 -0
  39. package/dist/ai/nodes/find-docs/node.d.ts +17 -0
  40. package/dist/ai/nodes/find-docs/node.d.ts.map +1 -0
  41. package/dist/ai/nodes/find-docs/node.js +46 -0
  42. package/dist/ai/nodes/find-docs/node.js.map +1 -0
  43. package/dist/ai/nodes/generic-chat/generic-chat.node.d.ts +12 -0
  44. package/dist/ai/nodes/generic-chat/generic-chat.node.d.ts.map +1 -0
  45. package/dist/ai/nodes/generic-chat/generic-chat.node.js +34 -0
  46. package/dist/ai/nodes/generic-chat/generic-chat.node.js.map +1 -0
  47. package/dist/ai/nodes/generic-chat/generic-chat.prompt.d.ts +10 -0
  48. package/dist/ai/nodes/generic-chat/generic-chat.prompt.d.ts.map +1 -0
  49. package/dist/ai/nodes/generic-chat/generic-chat.prompt.js +58 -0
  50. package/dist/ai/nodes/generic-chat/generic-chat.prompt.js.map +1 -0
  51. package/dist/ai/nodes/generic-chat/index.d.ts +2 -0
  52. package/dist/ai/nodes/generic-chat/index.d.ts.map +1 -0
  53. package/dist/ai/nodes/generic-chat/index.js +2 -0
  54. package/dist/ai/nodes/generic-chat/index.js.map +1 -0
  55. package/dist/ai/nodes/index.d.ts +4 -0
  56. package/dist/ai/nodes/index.d.ts.map +1 -0
  57. package/dist/ai/nodes/index.js +4 -0
  58. package/dist/ai/nodes/index.js.map +1 -0
  59. package/dist/ai/semantic-router-factory/create-semantic-router.d.ts +7 -0
  60. package/dist/ai/semantic-router-factory/create-semantic-router.d.ts.map +1 -0
  61. package/dist/ai/semantic-router-factory/create-semantic-router.js +71 -0
  62. package/dist/ai/semantic-router-factory/create-semantic-router.js.map +1 -0
  63. package/dist/ai/semantic-router-factory/create-semantic-router.test.d.ts +2 -0
  64. package/dist/ai/semantic-router-factory/create-semantic-router.test.d.ts.map +1 -0
  65. package/dist/ai/semantic-router-factory/create-semantic-router.test.js +68 -0
  66. package/dist/ai/semantic-router-factory/create-semantic-router.test.js.map +1 -0
  67. package/dist/ai/semantic-router-factory/index.d.ts +4 -0
  68. package/dist/ai/semantic-router-factory/index.d.ts.map +1 -0
  69. package/dist/ai/semantic-router-factory/index.js +4 -0
  70. package/dist/ai/semantic-router-factory/index.js.map +1 -0
  71. package/dist/ai/semantic-router-factory/semantic-router-prompt.d.ts +2 -0
  72. package/dist/ai/semantic-router-factory/semantic-router-prompt.d.ts.map +1 -0
  73. package/dist/ai/semantic-router-factory/semantic-router-prompt.js +169 -0
  74. package/dist/ai/semantic-router-factory/semantic-router-prompt.js.map +1 -0
  75. package/dist/ai/semantic-router-factory/validate-routes.d.ts +2 -0
  76. package/dist/ai/semantic-router-factory/validate-routes.d.ts.map +1 -0
  77. package/dist/ai/semantic-router-factory/validate-routes.js +16 -0
  78. package/dist/ai/semantic-router-factory/validate-routes.js.map +1 -0
  79. package/dist/ai/tools/action-caller.d.ts +14 -0
  80. package/dist/ai/tools/action-caller.d.ts.map +1 -0
  81. package/dist/ai/tools/action-caller.js +12 -0
  82. package/dist/ai/tools/action-caller.js.map +1 -0
  83. package/dist/ai/tools/ask-ixo-guru/ask-ixo-guru.d.ts +25 -0
  84. package/dist/ai/tools/ask-ixo-guru/ask-ixo-guru.d.ts.map +1 -0
  85. package/dist/ai/tools/ask-ixo-guru/ask-ixo-guru.js +46 -0
  86. package/dist/ai/tools/ask-ixo-guru/ask-ixo-guru.js.map +1 -0
  87. package/dist/ai/tools/ask-ixo-guru/index.d.ts +2 -0
  88. package/dist/ai/tools/ask-ixo-guru/index.d.ts.map +1 -0
  89. package/dist/ai/tools/ask-ixo-guru/index.js +2 -0
  90. package/dist/ai/tools/ask-ixo-guru/index.js.map +1 -0
  91. package/dist/ai/tools/browser-tool-caller.d.ts +14 -0
  92. package/dist/ai/tools/browser-tool-caller.d.ts.map +1 -0
  93. package/dist/ai/tools/browser-tool-caller.js +12 -0
  94. package/dist/ai/tools/browser-tool-caller.js.map +1 -0
  95. package/dist/ai/tools/frontend-tool-caller.d.ts +10 -0
  96. package/dist/ai/tools/frontend-tool-caller.d.ts.map +1 -0
  97. package/dist/ai/tools/frontend-tool-caller.js +50 -0
  98. package/dist/ai/tools/frontend-tool-caller.js.map +1 -0
  99. package/dist/ai/tools/index.d.ts +11 -0
  100. package/dist/ai/tools/index.d.ts.map +1 -0
  101. package/dist/ai/tools/index.js +11 -0
  102. package/dist/ai/tools/index.js.map +1 -0
  103. package/dist/ai/tools/log-action-to-matrix.d.ts +13 -0
  104. package/dist/ai/tools/log-action-to-matrix.d.ts.map +1 -0
  105. package/dist/ai/tools/log-action-to-matrix.js +14 -0
  106. package/dist/ai/tools/log-action-to-matrix.js.map +1 -0
  107. package/dist/ai/tools/parser-action-tool.d.ts +8 -0
  108. package/dist/ai/tools/parser-action-tool.d.ts.map +1 -0
  109. package/dist/ai/tools/parser-action-tool.js +40 -0
  110. package/dist/ai/tools/parser-action-tool.js.map +1 -0
  111. package/dist/ai/tools/parser-browser-tool.d.ts +8 -0
  112. package/dist/ai/tools/parser-browser-tool.d.ts.map +1 -0
  113. package/dist/ai/tools/parser-browser-tool.js +38 -0
  114. package/dist/ai/tools/parser-browser-tool.js.map +1 -0
  115. package/dist/ai/tools/retriever-tool/index.d.ts +2 -0
  116. package/dist/ai/tools/retriever-tool/index.d.ts.map +1 -0
  117. package/dist/ai/tools/retriever-tool/index.js +2 -0
  118. package/dist/ai/tools/retriever-tool/index.js.map +1 -0
  119. package/dist/ai/tools/retriever-tool/retriever-tool.d.ts +18 -0
  120. package/dist/ai/tools/retriever-tool/retriever-tool.d.ts.map +1 -0
  121. package/dist/ai/tools/retriever-tool/retriever-tool.js +62 -0
  122. package/dist/ai/tools/retriever-tool/retriever-tool.js.map +1 -0
  123. package/dist/ai/tools/retriever-tool/retriever-tool.test.d.ts +2 -0
  124. package/dist/ai/tools/retriever-tool/retriever-tool.test.d.ts.map +1 -0
  125. package/dist/ai/tools/retriever-tool/retriever-tool.test.js +119 -0
  126. package/dist/ai/tools/retriever-tool/retriever-tool.test.js.map +1 -0
  127. package/dist/ai/tools/scrape-web-page.d.ts +7 -0
  128. package/dist/ai/tools/scrape-web-page.d.ts.map +1 -0
  129. package/dist/ai/tools/scrape-web-page.js +65 -0
  130. package/dist/ai/tools/scrape-web-page.js.map +1 -0
  131. package/dist/ai/tools/web-search-tool.d.ts +10 -0
  132. package/dist/ai/tools/web-search-tool.d.ts.map +1 -0
  133. package/dist/ai/tools/web-search-tool.js +30 -0
  134. package/dist/ai/tools/web-search-tool.js.map +1 -0
  135. package/dist/ai/types.d.ts +4 -0
  136. package/dist/ai/types.d.ts.map +1 -0
  137. package/dist/ai/types.js +2 -0
  138. package/dist/ai/types.js.map +1 -0
  139. package/dist/ai/utils/__tests__/chunk-arr.test.d.ts +2 -0
  140. package/dist/ai/utils/__tests__/chunk-arr.test.d.ts.map +1 -0
  141. package/dist/ai/utils/__tests__/chunk-arr.test.js +37 -0
  142. package/dist/ai/utils/__tests__/chunk-arr.test.js.map +1 -0
  143. package/dist/ai/utils/__tests__/doc-relevance-checker.test.d.ts +2 -0
  144. package/dist/ai/utils/__tests__/doc-relevance-checker.test.d.ts.map +1 -0
  145. package/dist/ai/utils/__tests__/doc-relevance-checker.test.js +80 -0
  146. package/dist/ai/utils/__tests__/doc-relevance-checker.test.js.map +1 -0
  147. package/dist/ai/utils/__tests__/doc-splitter.test.d.ts +2 -0
  148. package/dist/ai/utils/__tests__/doc-splitter.test.d.ts.map +1 -0
  149. package/dist/ai/utils/__tests__/doc-splitter.test.js +35 -0
  150. package/dist/ai/utils/__tests__/doc-splitter.test.js.map +1 -0
  151. package/dist/ai/utils/__tests__/filter-similarity-search-results.test.d.ts +2 -0
  152. package/dist/ai/utils/__tests__/filter-similarity-search-results.test.d.ts.map +1 -0
  153. package/dist/ai/utils/__tests__/filter-similarity-search-results.test.js +47 -0
  154. package/dist/ai/utils/__tests__/filter-similarity-search-results.test.js.map +1 -0
  155. package/dist/ai/utils/__tests__/json-to-yaml.test.d.ts +2 -0
  156. package/dist/ai/utils/__tests__/json-to-yaml.test.d.ts.map +1 -0
  157. package/dist/ai/utils/__tests__/json-to-yaml.test.js +63 -0
  158. package/dist/ai/utils/__tests__/json-to-yaml.test.js.map +1 -0
  159. package/dist/ai/utils/__tests__/stringify-docs.test.d.ts +2 -0
  160. package/dist/ai/utils/__tests__/stringify-docs.test.d.ts.map +1 -0
  161. package/dist/ai/utils/__tests__/stringify-docs.test.js +54 -0
  162. package/dist/ai/utils/__tests__/stringify-docs.test.js.map +1 -0
  163. package/dist/ai/utils/chunk-arr.d.ts +3 -0
  164. package/dist/ai/utils/chunk-arr.d.ts.map +1 -0
  165. package/dist/ai/utils/chunk-arr.js +13 -0
  166. package/dist/ai/utils/chunk-arr.js.map +1 -0
  167. package/dist/ai/utils/doc-relevance-checker.d.ts +10 -0
  168. package/dist/ai/utils/doc-relevance-checker.d.ts.map +1 -0
  169. package/dist/ai/utils/doc-relevance-checker.js +37 -0
  170. package/dist/ai/utils/doc-relevance-checker.js.map +1 -0
  171. package/dist/ai/utils/doc-splitter.d.ts +3 -0
  172. package/dist/ai/utils/doc-splitter.d.ts.map +1 -0
  173. package/dist/ai/utils/doc-splitter.js +20 -0
  174. package/dist/ai/utils/doc-splitter.js.map +1 -0
  175. package/dist/ai/utils/filter-similarity-search-results.d.ts +4 -0
  176. package/dist/ai/utils/filter-similarity-search-results.d.ts.map +1 -0
  177. package/dist/ai/utils/filter-similarity-search-results.js +11 -0
  178. package/dist/ai/utils/filter-similarity-search-results.js.map +1 -0
  179. package/dist/ai/utils/generate-questions-from-chunks.d.ts +15 -0
  180. package/dist/ai/utils/generate-questions-from-chunks.d.ts.map +1 -0
  181. package/dist/ai/utils/generate-questions-from-chunks.js +101 -0
  182. package/dist/ai/utils/generate-questions-from-chunks.js.map +1 -0
  183. package/dist/ai/utils/index.d.ts +11 -0
  184. package/dist/ai/utils/index.d.ts.map +1 -0
  185. package/dist/ai/utils/index.js +11 -0
  186. package/dist/ai/utils/index.js.map +1 -0
  187. package/dist/ai/utils/json-to-yaml.d.ts +2 -0
  188. package/dist/ai/utils/json-to-yaml.d.ts.map +1 -0
  189. package/dist/ai/utils/json-to-yaml.js +25 -0
  190. package/dist/ai/utils/json-to-yaml.js.map +1 -0
  191. package/dist/ai/utils/load-file.d.ts +3 -0
  192. package/dist/ai/utils/load-file.d.ts.map +1 -0
  193. package/dist/ai/utils/load-file.js +133 -0
  194. package/dist/ai/utils/load-file.js.map +1 -0
  195. package/dist/ai/utils/stringify-docs.d.ts +3 -0
  196. package/dist/ai/utils/stringify-docs.d.ts.map +1 -0
  197. package/dist/ai/utils/stringify-docs.js +6 -0
  198. package/dist/ai/utils/stringify-docs.js.map +1 -0
  199. package/dist/ai/utils/transformGraphStateMessageToListMessageResponse.d.ts +35 -0
  200. package/dist/ai/utils/transformGraphStateMessageToListMessageResponse.d.ts.map +1 -0
  201. package/dist/ai/utils/transformGraphStateMessageToListMessageResponse.js +55 -0
  202. package/dist/ai/utils/transformGraphStateMessageToListMessageResponse.js.map +1 -0
  203. package/dist/ai/utils/verify-matrix-openId-token.d.ts +6 -0
  204. package/dist/ai/utils/verify-matrix-openId-token.d.ts.map +1 -0
  205. package/dist/ai/utils/verify-matrix-openId-token.js +36 -0
  206. package/dist/ai/utils/verify-matrix-openId-token.js.map +1 -0
  207. package/dist/index.d.ts +4 -0
  208. package/dist/index.d.ts.map +1 -0
  209. package/dist/index.js +4 -0
  210. package/dist/index.js.map +1 -0
  211. package/dist/services/env/env-service.test.d.ts +2 -0
  212. package/dist/services/env/env-service.test.d.ts.map +1 -0
  213. package/dist/services/env/env-service.test.js +99 -0
  214. package/dist/services/env/env-service.test.js.map +1 -0
  215. package/dist/services/env/env.service.d.ts +11 -0
  216. package/dist/services/env/env.service.d.ts.map +1 -0
  217. package/dist/services/env/env.service.js +42 -0
  218. package/dist/services/env/env.service.js.map +1 -0
  219. package/dist/services/env/index.d.ts +2 -0
  220. package/dist/services/env/index.d.ts.map +1 -0
  221. package/dist/services/env/index.js +2 -0
  222. package/dist/services/env/index.js.map +1 -0
  223. package/dist/services/index.d.ts +5 -0
  224. package/dist/services/index.d.ts.map +1 -0
  225. package/dist/services/index.js +5 -0
  226. package/dist/services/index.js.map +1 -0
  227. package/dist/services/memory-engine/memory-engine.service.d.ts +35 -0
  228. package/dist/services/memory-engine/memory-engine.service.d.ts.map +1 -0
  229. package/dist/services/memory-engine/memory-engine.service.js +295 -0
  230. package/dist/services/memory-engine/memory-engine.service.js.map +1 -0
  231. package/dist/services/memory-engine/types.d.ts +88 -0
  232. package/dist/services/memory-engine/types.d.ts.map +1 -0
  233. package/dist/services/memory-engine/types.js +2 -0
  234. package/dist/services/memory-engine/types.js.map +1 -0
  235. package/dist/services/session-manager/dto.d.ts +40 -0
  236. package/dist/services/session-manager/dto.d.ts.map +1 -0
  237. package/dist/services/session-manager/dto.js +170 -0
  238. package/dist/services/session-manager/dto.js.map +1 -0
  239. package/dist/services/session-manager/errors.d.ts +22 -0
  240. package/dist/services/session-manager/errors.d.ts.map +1 -0
  241. package/dist/services/session-manager/errors.js +41 -0
  242. package/dist/services/session-manager/errors.js.map +1 -0
  243. package/dist/services/session-manager/index.d.ts +4 -0
  244. package/dist/services/session-manager/index.d.ts.map +1 -0
  245. package/dist/services/session-manager/index.js +4 -0
  246. package/dist/services/session-manager/index.js.map +1 -0
  247. package/dist/services/session-manager/session-manager.service.d.ts +40 -0
  248. package/dist/services/session-manager/session-manager.service.d.ts.map +1 -0
  249. package/dist/services/session-manager/session-manager.service.js +251 -0
  250. package/dist/services/session-manager/session-manager.service.js.map +1 -0
  251. package/dist/utils/get-user-subscription.d.ts +21 -0
  252. package/dist/utils/get-user-subscription.d.ts.map +1 -0
  253. package/dist/utils/get-user-subscription.js +44 -0
  254. package/dist/utils/get-user-subscription.js.map +1 -0
  255. package/dist/utils/index.d.ts +2 -0
  256. package/dist/utils/index.d.ts.map +1 -0
  257. package/dist/utils/index.js +2 -0
  258. package/dist/utils/index.js.map +1 -0
  259. package/docs/ai-module.md +84 -0
  260. package/docs/services.md +168 -0
  261. package/docs/tools.md +325 -0
  262. package/jest.config.js +6 -0
  263. package/package.json +81 -0
  264. package/src/ai/checkpointer/index.ts +1 -0
  265. package/src/ai/index.ts +8 -0
  266. package/src/ai/models/index.ts +1 -0
  267. package/src/ai/models/openai.test.ts +72 -0
  268. package/src/ai/models/openai.ts +54 -0
  269. package/src/ai/nodes/create-fake-node.ts +1 -0
  270. package/src/ai/nodes/find-docs/find-docs.prompt.ts +61 -0
  271. package/src/ai/nodes/find-docs/index.ts +2 -0
  272. package/src/ai/nodes/find-docs/node.ts +83 -0
  273. package/src/ai/nodes/generic-chat/generic-chat.node.ts +58 -0
  274. package/src/ai/nodes/generic-chat/generic-chat.prompt.ts +66 -0
  275. package/src/ai/nodes/generic-chat/index.ts +1 -0
  276. package/src/ai/nodes/index.ts +3 -0
  277. package/src/ai/semantic-router-factory/create-semantic-router.test.ts +98 -0
  278. package/src/ai/semantic-router-factory/create-semantic-router.ts +136 -0
  279. package/src/ai/semantic-router-factory/index.ts +3 -0
  280. package/src/ai/semantic-router-factory/semantic-router-prompt.ts +168 -0
  281. package/src/ai/semantic-router-factory/validate-routes.ts +26 -0
  282. package/src/ai/tools/action-caller.ts +37 -0
  283. package/src/ai/tools/ask-ixo-guru/ask-ixo-guru.ts +73 -0
  284. package/src/ai/tools/ask-ixo-guru/index.ts +1 -0
  285. package/src/ai/tools/browser-tool-caller.ts +37 -0
  286. package/src/ai/tools/frontend-tool-caller.ts +86 -0
  287. package/src/ai/tools/index.ts +10 -0
  288. package/src/ai/tools/log-action-to-matrix.ts +30 -0
  289. package/src/ai/tools/parser-action-tool.ts +61 -0
  290. package/src/ai/tools/parser-browser-tool.ts +55 -0
  291. package/src/ai/tools/retriever-tool/index.ts +1 -0
  292. package/src/ai/tools/retriever-tool/retriever-tool.test.ts +156 -0
  293. package/src/ai/tools/retriever-tool/retriever-tool.ts +107 -0
  294. package/src/ai/tools/scrape-web-page.ts +75 -0
  295. package/src/ai/tools/web-search-tool.ts +38 -0
  296. package/src/ai/types.ts +6 -0
  297. package/src/ai/utils/__tests__/chunk-arr.test.ts +46 -0
  298. package/src/ai/utils/__tests__/doc-relevance-checker.test.ts +90 -0
  299. package/src/ai/utils/__tests__/doc-splitter.test.ts +42 -0
  300. package/src/ai/utils/__tests__/filter-similarity-search-results.test.ts +57 -0
  301. package/src/ai/utils/__tests__/json-to-yaml.test.ts +70 -0
  302. package/src/ai/utils/__tests__/stringify-docs.test.ts +61 -0
  303. package/src/ai/utils/chunk-arr.ts +13 -0
  304. package/src/ai/utils/doc-relevance-checker.ts +58 -0
  305. package/src/ai/utils/doc-splitter.ts +28 -0
  306. package/src/ai/utils/filter-similarity-search-results.ts +15 -0
  307. package/src/ai/utils/generate-questions-from-chunks.ts +114 -0
  308. package/src/ai/utils/index.ts +10 -0
  309. package/src/ai/utils/json-to-yaml.ts +32 -0
  310. package/src/ai/utils/load-file.ts +170 -0
  311. package/src/ai/utils/stringify-docs.ts +14 -0
  312. package/src/ai/utils/transformGraphStateMessageToListMessageResponse.ts +108 -0
  313. package/src/ai/utils/verify-matrix-openId-token.ts +46 -0
  314. package/src/index.ts +3 -0
  315. package/src/services/env/env-service.test.ts +153 -0
  316. package/src/services/env/env.service.ts +65 -0
  317. package/src/services/env/index.ts +1 -0
  318. package/src/services/index.ts +4 -0
  319. package/src/services/memory-engine/memory-engine.service.ts +486 -0
  320. package/src/services/memory-engine/types.ts +208 -0
  321. package/src/services/session-manager/dto.ts +120 -0
  322. package/src/services/session-manager/errors.ts +56 -0
  323. package/src/services/session-manager/index.ts +3 -0
  324. package/src/services/session-manager/session-manager.service.ts +405 -0
  325. package/src/utils/get-user-subscription.ts +84 -0
  326. package/src/utils/index.ts +1 -0
  327. package/tsconfig.json +16 -0
  328. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,156 @@
1
+ import { type VectorDBDataStore } from '@ixo/data-store';
2
+ import { Document } from '@langchain/core/documents';
3
+ import { FakeChatModel } from '@langchain/core/utils/testing';
4
+ import { retrieverToolFactory } from './retriever-tool.js';
5
+
6
+ const fakeModel = new FakeChatModel({});
7
+
8
+ // Mock the logger to prevent console output during tests
9
+ jest.mock('@ixo/logger', () => ({
10
+ Logger: {
11
+ error: jest.fn(),
12
+ },
13
+ }));
14
+
15
+ // Mock the OpenAI model
16
+ jest.mock('../../models/openai', () => ({
17
+ getChatOpenAiModel: () => fakeModel,
18
+ }));
19
+
20
+ // Mock the doc relevance checker
21
+ jest.mock('../../utils/doc-relevance-checker', () => ({
22
+ __esModule: true,
23
+ default: jest.fn().mockResolvedValue(true),
24
+ }));
25
+ const fakeDocRelevanceChecker = jest.fn().mockResolvedValue(true);
26
+
27
+ jest
28
+ .spyOn(require('../../utils/doc-relevance-checker'), 'default')
29
+ .mockImplementation(fakeDocRelevanceChecker);
30
+
31
+ describe('RetrieverTool', () => {
32
+ let mockStore: jest.Mocked<VectorDBDataStore>;
33
+ let mockModel: FakeChatModel = new FakeChatModel({});
34
+
35
+ beforeEach(() => {
36
+ mockStore = {
37
+ queryWithSimilarity: jest.fn(),
38
+ } as unknown as jest.Mocked<VectorDBDataStore>;
39
+ });
40
+
41
+ it('should retrieve documents with default settings', async () => {
42
+ const mockDocs = [
43
+ { id: '1', content: 'test content 1', metadata: { source: 'test1' } },
44
+ { id: '2', content: 'test content 2', metadata: { source: 'test2' } },
45
+ ];
46
+
47
+ mockStore.queryWithSimilarity.mockResolvedValue(mockDocs);
48
+
49
+ const tool = retrieverToolFactory({ store: mockStore });
50
+ const result = (await tool.invoke({ query: 'test query' })) as Document[];
51
+
52
+ expect(mockStore.queryWithSimilarity).toHaveBeenCalledWith('test query', {
53
+ similarityThreshold: 0.3,
54
+ filters: undefined,
55
+ });
56
+
57
+ expect(result).toHaveLength(2);
58
+ expect(result[0]).toBeInstanceOf(Document);
59
+ expect(result[0]?.pageContent).toBe('test content 1');
60
+ expect(result[0]?.metadata).toEqual({ source: 'test1' });
61
+
62
+ expect(fakeDocRelevanceChecker).toHaveBeenCalledWith({
63
+ doc: result[0],
64
+ query: 'test query',
65
+ model: fakeModel,
66
+ });
67
+ });
68
+
69
+ it('should store results in provided map', async () => {
70
+ const mockDocs = [
71
+ { id: '1', content: 'test content', metadata: { source: 'test' } },
72
+ ];
73
+ mockStore.queryWithSimilarity.mockResolvedValue(mockDocs);
74
+
75
+ const resultsMap = new Map();
76
+ const requestId = 'test-request-id';
77
+
78
+ const tool = retrieverToolFactory({
79
+ store: mockStore,
80
+ map: resultsMap,
81
+ requestId,
82
+ });
83
+
84
+ await tool.invoke({ query: 'test query' });
85
+
86
+ expect(resultsMap.get(requestId)).toEqual([{ source: 'test' }]);
87
+ });
88
+
89
+ it('should return undefined when no documents found', async () => {
90
+ mockStore.queryWithSimilarity.mockResolvedValue([]);
91
+
92
+ const tool = retrieverToolFactory({ store: mockStore });
93
+ const result = await tool.invoke({ query: 'test query' });
94
+
95
+ expect(result).toBeUndefined();
96
+ });
97
+
98
+ it('should check document relevance when similarity threshold is higher', async () => {
99
+ const mockDocs = [
100
+ { id: '1', content: 'test content', metadata: { source: 'test' } },
101
+ ];
102
+ mockStore.queryWithSimilarity.mockResolvedValue(mockDocs);
103
+
104
+ const tool = retrieverToolFactory({
105
+ store: mockStore,
106
+ similarThreshold: 0.5,
107
+ model: mockModel,
108
+ });
109
+
110
+ const result = await tool.invoke({ query: 'test query' });
111
+
112
+ expect(result).toHaveLength(1);
113
+ expect(result[0]).toBeInstanceOf(Document);
114
+ });
115
+
116
+ it('should handle errors during relevance checking', async () => {
117
+ const mockDocs = [
118
+ { id: '1', content: 'test content', metadata: { source: 'test' } },
119
+ ];
120
+ mockStore.queryWithSimilarity.mockResolvedValue(mockDocs);
121
+
122
+ // Mock the relevance checker to throw an error
123
+ jest
124
+ .requireMock('../../utils/doc-relevance-checker')
125
+ .default.mockRejectedValueOnce(new Error('Test error'));
126
+
127
+ const tool = retrieverToolFactory({
128
+ store: mockStore,
129
+ similarThreshold: 0.5,
130
+ model: mockModel,
131
+ });
132
+
133
+ const result = await tool.invoke({ query: 'test query' });
134
+
135
+ expect(result).toHaveLength(0);
136
+ });
137
+
138
+ it('should apply filters when provided', async () => {
139
+ const mockDocs = [
140
+ { id: '1', content: 'test content', metadata: { source: 'test' } },
141
+ ];
142
+ mockStore.queryWithSimilarity.mockResolvedValue(mockDocs);
143
+ const filters = { category: 'test' };
144
+ const tool = retrieverToolFactory({
145
+ store: mockStore,
146
+ filters,
147
+ });
148
+
149
+ await tool.invoke({ query: 'test query' });
150
+
151
+ expect(mockStore.queryWithSimilarity).toHaveBeenCalledWith('test query', {
152
+ similarityThreshold: 0.3,
153
+ filters,
154
+ });
155
+ });
156
+ });
@@ -0,0 +1,107 @@
1
+ import { type VectorDBDataStore } from '@ixo/data-store';
2
+ import { Logger } from '@ixo/logger';
3
+ import { Document } from '@langchain/core/documents';
4
+ import { type BaseChatModel } from '@langchain/core/language_models/chat_models';
5
+ import { tool, type DynamicStructuredTool } from '@langchain/core/tools';
6
+ import z from 'zod';
7
+ import { getChatOpenAiModel } from '../../models/openai.js';
8
+ import checkDocRelevance from '../../utils/doc-relevance-checker.js';
9
+
10
+ type RetrieverToolFactoryArgs<
11
+ Filters extends Record<string, unknown> = Record<string, unknown>,
12
+ MapType extends Map<string, unknown> = Map<string, unknown>,
13
+ > = {
14
+ model?: BaseChatModel;
15
+ filters?: Filters;
16
+ similarThreshold?: number;
17
+ store: VectorDBDataStore;
18
+ map?: MapType;
19
+ requestId?: string;
20
+ };
21
+
22
+ const schema = z.object({
23
+ query: z.string(),
24
+ });
25
+
26
+ export const retrieverToolFactory = ({
27
+ model = getChatOpenAiModel(),
28
+ filters,
29
+ similarThreshold = 0.3,
30
+ map,
31
+ requestId,
32
+ store,
33
+ }: RetrieverToolFactoryArgs): DynamicStructuredTool<typeof schema> =>
34
+ tool(
35
+ async ({ query }) => {
36
+ const docs = await store.queryWithSimilarity(query, {
37
+ similarityThreshold: similarThreshold,
38
+ filters,
39
+ });
40
+
41
+ // save the results to the map if the parent class wan to access the docs used by the Agent
42
+ if (map) {
43
+ map.set(
44
+ requestId ?? query,
45
+ docs.map((doc) => doc.metadata),
46
+ );
47
+ }
48
+
49
+ if (docs.length === 0) {
50
+ return undefined;
51
+ }
52
+
53
+ if (similarThreshold >= 0.3) {
54
+ // Filter the docs based on relevance using the AI
55
+ const relevantDocs = await Promise.all(
56
+ docs.map(async (doc) => {
57
+ try {
58
+ return (
59
+ await checkDocRelevance({
60
+ doc: new Document({
61
+ pageContent: doc.content,
62
+ id: doc.id,
63
+ metadata: doc.metadata,
64
+ }),
65
+ query,
66
+ model,
67
+ })
68
+ ).valueOf()
69
+ ? doc
70
+ : null;
71
+ } catch (error) {
72
+ Logger.error(`Error checking relevance of document:`, error);
73
+ return null;
74
+ }
75
+ }),
76
+ );
77
+
78
+ return relevantDocs.reduce<Document[]>((acc, doc) => {
79
+ if (doc) {
80
+ acc.push(
81
+ new Document({
82
+ pageContent: doc.content,
83
+ id: doc.id,
84
+ metadata: doc.metadata,
85
+ }),
86
+ );
87
+ }
88
+ return acc;
89
+ }, []);
90
+ }
91
+
92
+ return docs.map(
93
+ (doc) =>
94
+ new Document({
95
+ pageContent: doc.content,
96
+ id: doc.id,
97
+ metadata: doc.metadata,
98
+ }),
99
+ );
100
+ },
101
+ {
102
+ name: 'retrieverTool',
103
+ description:
104
+ 'Retrieves documents from the knowledge base based on the query',
105
+ schema,
106
+ },
107
+ );
@@ -0,0 +1,75 @@
1
+ import { PlaywrightWebBaseLoader } from '@langchain/community/document_loaders/web/playwright';
2
+ import { HtmlToTextTransformer } from '@langchain/community/document_transformers/html_to_text';
3
+ import { Document } from '@langchain/core/documents';
4
+ import { PromptTemplate } from '@langchain/core/prompts';
5
+ import { ChatOpenAI } from '@langchain/openai';
6
+ import { createAgent } from 'langchain';
7
+
8
+ export const scrapeWebPage = async (url: string) => {
9
+ const content = await PlaywrightWebBaseLoader._scrape(url, {
10
+ launchOptions: {
11
+ headless: true,
12
+ },
13
+ gotoOptions: {
14
+ waitUntil: 'domcontentloaded',
15
+ },
16
+ });
17
+ const transformer = new HtmlToTextTransformer({});
18
+ const docs = await transformer.transformDocuments([
19
+ new Document({
20
+ pageContent: content,
21
+ metadata: {
22
+ source: url,
23
+ },
24
+ }),
25
+ ]);
26
+ return new Document({
27
+ pageContent: docs.map((doc) => doc.pageContent).join('\n'),
28
+ metadata: {
29
+ source: url,
30
+ },
31
+ });
32
+ };
33
+ const prompt = PromptTemplate.fromTemplate(
34
+ `
35
+ You are a helpful assistant that summarizes web pages.
36
+
37
+ #Guidelines
38
+ - Add key points at the beginning of the Document.
39
+ - Add the summary based on the key points.
40
+
41
+ #Rules
42
+ - Ignore the html tags and only focus on the content.
43
+ - Read the content of the page and summarize.
44
+ - DO NOT HALLUCINATE or make up information.
45
+ - DO NOT ADD ANYTHING ELSE OTHER THAN THE SUMMARY.
46
+
47
+ #Document
48
+ {context}
49
+ `,
50
+ );
51
+
52
+ export const scrapeAndSummarizeWebPage = async (
53
+ url: string,
54
+ llm: ChatOpenAI,
55
+ ) => {
56
+ const doc = await scrapeWebPage(url);
57
+
58
+ const agent = createAgent({
59
+ model: llm,
60
+ tools: [],
61
+ });
62
+
63
+ const result = await agent.invoke({
64
+ messages: [
65
+ {
66
+ role: 'user',
67
+ content: await prompt.format({
68
+ context: doc.pageContent,
69
+ }),
70
+ },
71
+ ],
72
+ });
73
+
74
+ return result;
75
+ };
@@ -0,0 +1,38 @@
1
+ import { tool } from '@langchain/core/tools';
2
+ import { tavily } from '@tavily/core';
3
+ import z from 'zod';
4
+ import { jsonToYaml } from '../utils/json-to-yaml.js';
5
+
6
+ const webSearchTool = tool(
7
+ async ({ input }: { input: string }) => {
8
+ const tvly = tavily({ apiKey: process.env.TAVILY_API_KEY });
9
+ const searchResults = await tvly.search(input, {
10
+ maxResults: 3,
11
+ includeAnswer: true,
12
+ });
13
+
14
+ const res = searchResults.results.map((result) =>
15
+ jsonToYaml({
16
+ title: result.title,
17
+ url: result.url,
18
+ content: result.content,
19
+ publishedAt: result.publishedDate,
20
+ }),
21
+ );
22
+ return jsonToYaml({
23
+ query: searchResults.query,
24
+ summary: searchResults.answer,
25
+ results: res,
26
+ }).replace(/[{}]/g, '');
27
+ },
28
+ {
29
+ name: 'web_search_tool',
30
+ description:
31
+ 'A search engine optimized for comprehensive, accurate, and trusted results. Useful for when you need to answer questions about current events. Input should be a search query.',
32
+ schema: z.object({
33
+ input: z.string('The query to search for'),
34
+ }),
35
+ },
36
+ );
37
+
38
+ export { webSearchTool };
@@ -0,0 +1,6 @@
1
+ export type EnsureKeys<
2
+ T extends Record<string, unknown>,
3
+ K extends (keyof T)[],
4
+ > = {
5
+ [P in K[number]]: T[P]; // At least one key 'K' from 'T'
6
+ } & Partial<T>; // Other keys of 'T' are optional
@@ -0,0 +1,46 @@
1
+ import { chunkArr } from '../chunk-arr.js';
2
+
3
+ describe('chunkArr', () => {
4
+ it('should split array into chunks of specified size', () => {
5
+ const array = [1, 2, 3, 4, 5, 6, 7];
6
+ const result = chunkArr(array, 3);
7
+ expect(result).toEqual([[1, 2, 3], [4, 5, 6], [7]]);
8
+ });
9
+
10
+ it('should handle empty arrays', () => {
11
+ const array: number[] = [];
12
+ const result = chunkArr(array, 2);
13
+ expect(result).toEqual([]);
14
+ });
15
+
16
+ it('should handle chunk size equal to array length', () => {
17
+ const array = [1, 2, 3];
18
+ const result = chunkArr(array, 3);
19
+ expect(result).toEqual([[1, 2, 3]]);
20
+ });
21
+
22
+ it('should handle chunk size larger than array length', () => {
23
+ const array = [1, 2];
24
+ const result = chunkArr(array, 3);
25
+ expect(result).toEqual([[1, 2]]);
26
+ });
27
+
28
+ it('should throw error for chunk size less than or equal to 0', () => {
29
+ const array = [1, 2, 3];
30
+ expect(() => chunkArr(array, 0)).toThrow(
31
+ 'Chunk size must be greater than 0',
32
+ );
33
+ expect(() => chunkArr(array, -1)).toThrow(
34
+ 'Chunk size must be greater than 0',
35
+ );
36
+ });
37
+
38
+ it('should work with different types', () => {
39
+ const array = ['a', 'b', 'c', 'd'];
40
+ const result = chunkArr(array, 2);
41
+ expect(result).toEqual([
42
+ ['a', 'b'],
43
+ ['c', 'd'],
44
+ ]);
45
+ });
46
+ });
@@ -0,0 +1,90 @@
1
+ import { Document } from '@langchain/core/documents';
2
+ import { type BaseChatModel } from '@langchain/core/language_models/chat_models';
3
+ import { ChatPromptTemplate } from '@langchain/core/prompts';
4
+ import { FakeChatModel } from '@langchain/core/utils/testing';
5
+ import checkDocRelevance from '../doc-relevance-checker.js';
6
+
7
+ jest.spyOn(ChatPromptTemplate, 'fromTemplate');
8
+
9
+ describe('checkDocRelevance', () => {
10
+ // Create a mock model that simulates relevance checking
11
+ const createMockModel = (returnValue: boolean): BaseChatModel => {
12
+ const mockModel = new FakeChatModel({});
13
+ mockModel.withStructuredOutput = jest.fn().mockReturnValue({
14
+ pipe: () => ({
15
+ invoke: async () => ({ answer: returnValue }),
16
+ }),
17
+ });
18
+ (ChatPromptTemplate.fromTemplate as jest.Mock).mockReturnValue({
19
+ pipe: () => ({
20
+ invoke: async () => ({ answer: returnValue }),
21
+ }),
22
+ });
23
+ return mockModel;
24
+ };
25
+
26
+ it('should return true for relevant document using string input', async () => {
27
+ const mockModel = createMockModel(true);
28
+
29
+ const result = await checkDocRelevance({
30
+ doc: 'This is a document about JavaScript programming',
31
+ query: 'JavaScript development',
32
+ model: mockModel,
33
+ });
34
+ expect(result).toBe(true);
35
+ });
36
+
37
+ it('should return false for irrelevant document using string input', async () => {
38
+ const mockModel = createMockModel(false);
39
+ const result = await checkDocRelevance({
40
+ doc: 'This is a document about cooking recipes',
41
+ query: 'JavaScript development',
42
+ model: mockModel,
43
+ });
44
+ expect(result).toBe(false);
45
+ });
46
+
47
+ it('should handle Document object input', async () => {
48
+ const mockModel = createMockModel(true);
49
+ const doc = new Document({
50
+ pageContent: 'This is a document about TypeScript',
51
+ metadata: { id: '1' },
52
+ });
53
+ const result = await checkDocRelevance({
54
+ doc,
55
+ query: 'TypeScript features',
56
+ model: mockModel,
57
+ });
58
+ expect(result).toBe(true);
59
+ });
60
+
61
+ it('should work with empty document content', async () => {
62
+ const mockModel = createMockModel(false);
63
+ const result = await checkDocRelevance({
64
+ doc: '',
65
+ query: 'Any query',
66
+ model: mockModel,
67
+ });
68
+ expect(result).toBe(false);
69
+ });
70
+
71
+ it('should work with empty query', async () => {
72
+ const mockModel = createMockModel(false);
73
+ const result = await checkDocRelevance({
74
+ doc: 'Some content',
75
+ query: '',
76
+ model: mockModel,
77
+ });
78
+ expect(result).toBe(false);
79
+ });
80
+
81
+ it('should handle special characters in document and query', async () => {
82
+ const mockModel = createMockModel(true);
83
+ const result = await checkDocRelevance({
84
+ doc: 'Content with \n newlines and \t tabs',
85
+ query: 'Query with \n newlines',
86
+ model: mockModel,
87
+ });
88
+ expect(result).toBe(true);
89
+ });
90
+ });
@@ -0,0 +1,42 @@
1
+ import { Document } from '@langchain/core/documents';
2
+ import { docSplitter } from '../doc-splitter.js';
3
+
4
+ describe('docSplitter', () => {
5
+ it('should split a single string into documents', async () => {
6
+ const text = 'This is a test document';
7
+ const result = await docSplitter(text);
8
+ expect(Array.isArray(result)).toBe(true);
9
+ expect(result.length).toBeGreaterThan(0);
10
+ expect(result[0]).toBeInstanceOf(Document);
11
+ });
12
+
13
+ it('should split an array of strings into documents', async () => {
14
+ const texts = ['First document', 'Second document'];
15
+ const result = await docSplitter(texts);
16
+ expect(Array.isArray(result)).toBe(true);
17
+ expect(result.length).toBeGreaterThan(0);
18
+ expect(result).toEqual(expect.arrayContaining([expect.any(Document)]));
19
+ });
20
+
21
+ it('should throw error for empty string', () => {
22
+ expect(() => docSplitter('')).toThrow('No text provided');
23
+ });
24
+
25
+ it('should throw error for empty array', () => {
26
+ expect(() => docSplitter([])).toThrow('Text array cannot be empty');
27
+ });
28
+
29
+ it('should throw error for null or undefined input', () => {
30
+ // @ts-expect-error testing invalid input
31
+ expect(() => docSplitter(null)).toThrow('No text provided');
32
+ // @ts-expect-error testing invalid input
33
+ expect(() => docSplitter(undefined)).toThrow('No text provided');
34
+ });
35
+
36
+ it('should handle long text correctly', async () => {
37
+ const longText = 'a'.repeat(2000);
38
+ const result = await docSplitter(longText);
39
+ expect(Array.isArray(result)).toBe(true);
40
+ expect(result.length).toBeGreaterThan(0);
41
+ });
42
+ });
@@ -0,0 +1,57 @@
1
+ import { Document } from '@langchain/core/documents';
2
+ import { filterSimilaritySearchResults } from '../filter-similarity-search-results.js';
3
+
4
+ describe('filterSimilaritySearchResults', () => {
5
+ const createDoc = (content: string): Document =>
6
+ new Document({ pageContent: content });
7
+
8
+ it('should filter results above threshold', () => {
9
+ const results = [
10
+ [createDoc('doc1'), 0.9],
11
+ [createDoc('doc2'), 0.7],
12
+ [createDoc('doc3'), 0.3],
13
+ ] as [Document, number][];
14
+
15
+ const filtered = filterSimilaritySearchResults(results, 0.5);
16
+ expect(filtered).toHaveLength(2);
17
+ expect(filtered[0]).toBeInstanceOf(Document);
18
+ expect(filtered[1]).toBeInstanceOf(Document);
19
+ });
20
+
21
+ it('should return empty array when no results meet threshold', () => {
22
+ const results = [
23
+ [createDoc('doc1'), 0.3],
24
+ [createDoc('doc2'), 0.2],
25
+ ] as [Document, number][];
26
+
27
+ const filtered = filterSimilaritySearchResults(results, 0.5);
28
+ expect(filtered).toHaveLength(0);
29
+ });
30
+
31
+ it('should handle empty results array', () => {
32
+ const results: [Document, number][] = [];
33
+ const filtered = filterSimilaritySearchResults(results, 0.5);
34
+ expect(filtered).toHaveLength(0);
35
+ });
36
+
37
+ it('should handle threshold of 0', () => {
38
+ const results = [
39
+ [createDoc('doc1'), 0.1],
40
+ [createDoc('doc2'), 0.2],
41
+ ] as [Document, number][];
42
+
43
+ const filtered = filterSimilaritySearchResults(results, 0);
44
+ expect(filtered).toHaveLength(2);
45
+ });
46
+
47
+ it('should handle threshold of 1', () => {
48
+ const results = [
49
+ [createDoc('doc1'), 0.9],
50
+ [createDoc('doc2'), 1.0],
51
+ ] as [Document, number][];
52
+
53
+ const filtered = filterSimilaritySearchResults(results, 1);
54
+ expect(filtered).toHaveLength(1);
55
+ expect(filtered[0]).toBeInstanceOf(Document);
56
+ });
57
+ });
@@ -0,0 +1,70 @@
1
+ import { jsonToYaml } from '../json-to-yaml.js';
2
+
3
+ describe('jsonToYaml', () => {
4
+ it('should convert flat JSON object to YAML', () => {
5
+ const json = {
6
+ name: 'John',
7
+ age: 30,
8
+ city: 'New York',
9
+ };
10
+ const expected = 'name: John\nage: 30\ncity: New York';
11
+ expect(jsonToYaml(json)).toBe(expected);
12
+ });
13
+
14
+ it('should handle nested objects', () => {
15
+ const json = {
16
+ person: {
17
+ name: 'John',
18
+ address: {
19
+ city: 'New York',
20
+ zip: '10001',
21
+ },
22
+ },
23
+ };
24
+ const expected =
25
+ 'person:\n name: John\n address:\n city: New York\n zip: 10001';
26
+ expect(jsonToYaml(json)).toBe(expected);
27
+ });
28
+
29
+ it('should handle arrays as nested objects', () => {
30
+ const json = {
31
+ fruits: ['apple', 'banana', 'orange'],
32
+ };
33
+ const expected = 'fruits:\n 0: apple\n 1: banana\n 2: orange';
34
+ expect(jsonToYaml(json)).toBe(expected);
35
+ });
36
+
37
+ it('should handle empty objects', () => {
38
+ const json = {};
39
+ expect(jsonToYaml(json)).toBe('');
40
+ });
41
+
42
+ it('should handle null values', () => {
43
+ const json = {
44
+ name: null,
45
+ age: 30,
46
+ };
47
+ const expected = 'name: null\nage: 30';
48
+ expect(jsonToYaml(json)).toBe(expected);
49
+ });
50
+
51
+ it('should handle mixed types', () => {
52
+ const json = {
53
+ string: 'text',
54
+ number: 42,
55
+ boolean: true,
56
+ nullValue: null,
57
+ };
58
+ const expected = 'string: text\nnumber: 42\nboolean: true\nnullValue: null';
59
+ expect(jsonToYaml(json)).toBe(expected);
60
+ });
61
+
62
+ it('should handle custom indentation level', () => {
63
+ const json = {
64
+ name: 'John',
65
+ age: 30,
66
+ };
67
+ const expected = ' name: John\n age: 30';
68
+ expect(jsonToYaml(json, 2)).toBe(expected);
69
+ });
70
+ });