@far-world-labs/verblets 0.1.1 → 0.1.3

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 (300) hide show
  1. package/.cursor/launch.json +30 -0
  2. package/.cursor/settings.json +20 -0
  3. package/.github/workflows/branch-protection.yml +22 -0
  4. package/.github/workflows/ci.yml +120 -0
  5. package/.prettierrc +6 -0
  6. package/.release-it.json +4 -1
  7. package/.vscode/launch.json +31 -0
  8. package/AGENTS.md +220 -0
  9. package/DEVELOPING.md +105 -0
  10. package/README.md +254 -0
  11. package/eslint.config.js +80 -0
  12. package/package.json +29 -17
  13. package/scripts/generate-test/index.js +29 -3
  14. package/scripts/runner/index.js +26 -0
  15. package/scripts/simple-editor/index.js +29 -18
  16. package/scripts/summarize-files/index.js +28 -4
  17. package/src/chains/README.md +30 -0
  18. package/src/chains/anonymize/README.md +21 -0
  19. package/src/chains/anonymize/index.examples.js +75 -0
  20. package/src/chains/anonymize/index.js +121 -0
  21. package/src/chains/anonymize/index.spec.js +78 -0
  22. package/src/chains/bulk-central-tendency/index.examples.js +138 -0
  23. package/src/chains/bulk-central-tendency/index.js +91 -0
  24. package/src/chains/bulk-filter/README.md +21 -0
  25. package/src/chains/bulk-filter/index.examples.js +22 -0
  26. package/src/chains/bulk-filter/index.js +58 -0
  27. package/src/chains/bulk-filter/index.spec.js +38 -0
  28. package/src/chains/bulk-find/README.md +16 -0
  29. package/src/chains/bulk-find/index.examples.js +20 -0
  30. package/src/chains/bulk-find/index.js +30 -0
  31. package/src/chains/bulk-find/index.spec.js +26 -0
  32. package/src/chains/bulk-group/README.md +23 -0
  33. package/src/chains/bulk-group/index.examples.js +18 -0
  34. package/src/chains/bulk-group/index.js +34 -0
  35. package/src/chains/bulk-group/index.spec.js +41 -0
  36. package/src/chains/bulk-map/README.md +43 -0
  37. package/src/chains/bulk-map/index.examples.js +17 -0
  38. package/src/chains/bulk-map/index.js +86 -0
  39. package/src/chains/bulk-map/index.spec.js +44 -0
  40. package/src/chains/bulk-reduce/README.md +12 -0
  41. package/src/chains/bulk-reduce/index.examples.js +15 -0
  42. package/src/chains/bulk-reduce/index.js +13 -0
  43. package/src/chains/bulk-reduce/index.spec.js +25 -0
  44. package/src/chains/bulk-score/README.md +16 -0
  45. package/src/chains/bulk-score/bulk-score-result.json +18 -0
  46. package/src/chains/bulk-score/index.examples.js +22 -0
  47. package/src/chains/bulk-score/index.js +133 -0
  48. package/src/chains/bulk-score/index.spec.js +30 -0
  49. package/src/chains/category-samples/README.md +61 -0
  50. package/src/chains/category-samples/index.examples.js +103 -0
  51. package/src/chains/category-samples/index.js +134 -0
  52. package/src/chains/collect-terms/README.md +12 -0
  53. package/src/chains/collect-terms/index.examples.js +16 -0
  54. package/src/chains/collect-terms/index.js +44 -0
  55. package/src/chains/collect-terms/index.spec.js +25 -0
  56. package/src/chains/date/README.md +12 -0
  57. package/src/chains/date/index.examples.js +47 -0
  58. package/src/chains/date/index.js +74 -0
  59. package/src/chains/date/index.spec.js +62 -0
  60. package/src/chains/disambiguate/README.md +22 -0
  61. package/src/chains/disambiguate/disambiguate-meanings-result.json +16 -0
  62. package/src/chains/disambiguate/index.examples.js +18 -0
  63. package/src/chains/disambiguate/index.js +92 -0
  64. package/src/chains/disambiguate/index.spec.js +25 -0
  65. package/src/chains/dismantle/README.md +67 -0
  66. package/src/chains/dismantle/dismantle.examples.js +27 -0
  67. package/src/chains/dismantle/index.js +6 -17
  68. package/src/chains/dismantle/index.spec.js +1 -2
  69. package/src/chains/expect/README.md +171 -0
  70. package/src/chains/expect/index.examples.js +146 -0
  71. package/src/chains/expect/index.js +173 -0
  72. package/src/chains/expect/index.spec.js +324 -0
  73. package/src/chains/filter-ambiguous/README.md +11 -0
  74. package/src/chains/filter-ambiguous/index.examples.js +20 -0
  75. package/src/chains/filter-ambiguous/index.js +49 -0
  76. package/src/chains/filter-ambiguous/index.spec.js +31 -0
  77. package/src/chains/glossary/README.md +19 -0
  78. package/src/chains/glossary/index.examples.js +386 -0
  79. package/src/chains/glossary/index.js +75 -0
  80. package/src/chains/glossary/index.spec.js +19 -0
  81. package/src/chains/intersections/README.md +152 -0
  82. package/src/chains/intersections/index.examples.js +279 -0
  83. package/src/chains/intersections/index.js +366 -0
  84. package/src/chains/intersections/intersection-result.json +38 -0
  85. package/src/chains/list/index.examples.js +12 -16
  86. package/src/chains/list/index.js +106 -53
  87. package/src/chains/list/index.spec.js +8 -9
  88. package/src/chains/list/list-result.json +16 -0
  89. package/src/chains/llm-logger/README.md +208 -0
  90. package/src/chains/llm-logger/index.js +205 -0
  91. package/src/chains/llm-logger/index.spec.js +330 -0
  92. package/src/chains/questions/index.examples.js +2 -1
  93. package/src/chains/questions/index.js +14 -15
  94. package/src/chains/scan-js/index.js +6 -9
  95. package/src/chains/set-interval/README.md +81 -0
  96. package/src/chains/set-interval/index.examples.js +36 -0
  97. package/src/chains/set-interval/index.js +131 -0
  98. package/src/chains/set-interval/index.spec.js +70 -0
  99. package/src/chains/socratic/README.md +17 -0
  100. package/src/chains/socratic/index.js +64 -0
  101. package/src/chains/socratic/index.spec.js +24 -0
  102. package/src/chains/sort/index.examples.js +3 -7
  103. package/src/chains/sort/index.js +65 -15
  104. package/src/chains/sort/index.spec.js +5 -8
  105. package/src/chains/sort/sort-result.json +16 -0
  106. package/src/chains/summary-map/README.md +9 -1
  107. package/src/chains/summary-map/index.examples.js +9 -2
  108. package/src/chains/summary-map/index.js +43 -25
  109. package/src/chains/summary-map/index.spec.js +78 -3
  110. package/src/chains/test/index.js +9 -13
  111. package/src/chains/test-advice/index.js +4 -5
  112. package/src/chains/themes/README.md +20 -0
  113. package/src/chains/themes/index.examples.js +17 -0
  114. package/src/chains/themes/index.js +28 -0
  115. package/src/chains/themes/index.spec.js +19 -0
  116. package/src/chains/veiled-variants/index.examples.js +18 -0
  117. package/src/chains/veiled-variants/index.js +107 -0
  118. package/src/chains/veiled-variants/index.spec.js +40 -0
  119. package/src/constants/common.js +0 -2
  120. package/src/constants/models.js +172 -0
  121. package/src/index.js +178 -18
  122. package/src/json-schemas/README.md +13 -0
  123. package/src/json-schemas/index.js +8 -14
  124. package/src/json-schemas/schema-dot-org-photograph.json +11 -5
  125. package/src/json-schemas/schema-dot-org-place.json +78 -5
  126. package/src/lib/README.md +26 -0
  127. package/src/lib/bulk-filter/README.md +22 -0
  128. package/src/lib/bulk-filter/index.examples.js +27 -0
  129. package/src/lib/bulk-filter/index.js +63 -0
  130. package/src/lib/bulk-filter/index.spec.js +38 -0
  131. package/src/lib/bulk-find/README.md +18 -0
  132. package/src/lib/bulk-find/index.examples.js +19 -0
  133. package/src/lib/bulk-find/index.js +30 -0
  134. package/src/lib/bulk-find/index.spec.js +41 -0
  135. package/src/lib/chatgpt/index.js +63 -43
  136. package/src/lib/combinations/index.js +30 -0
  137. package/src/lib/combinations/index.spec.js +23 -0
  138. package/src/lib/functional/index.js +28 -0
  139. package/src/lib/logger-service/index.js +32 -0
  140. package/src/lib/parse-js-parts/index.js +9 -21
  141. package/src/lib/parse-llm-list/README.md +39 -0
  142. package/src/lib/parse-llm-list/index.js +54 -0
  143. package/src/lib/parse-llm-list/index.spec.js +59 -0
  144. package/src/lib/path-aliases/index.js +1 -3
  145. package/src/lib/path-aliases/index.spec.js +2 -8
  146. package/src/lib/pave/index.js +4 -4
  147. package/src/lib/pave/index.spec.js +6 -3
  148. package/src/lib/prompt-cache/index.js +14 -10
  149. package/src/lib/retry/index.js +11 -8
  150. package/src/lib/ring-buffer/README.md +460 -0
  151. package/src/lib/ring-buffer/index.js +1074 -0
  152. package/src/lib/search-best-first/city-walk.spec.js +37 -0
  153. package/src/lib/search-best-first/index.js +42 -11
  154. package/src/lib/search-best-first/index.spec.js +35 -0
  155. package/src/lib/search-js-files/index.js +44 -47
  156. package/src/lib/search-js-files/scan-file.js +10 -21
  157. package/src/lib/shorten-text/index.js +2 -7
  158. package/src/lib/shorten-text/index.spec.js +3 -3
  159. package/src/lib/strip-response/index.js +2 -7
  160. package/src/lib/template-replace/index.js +23 -0
  161. package/src/lib/template-replace/index.spec.js +60 -0
  162. package/src/lib/to-date/index.js +11 -0
  163. package/src/lib/to-number/index.js +1 -1
  164. package/src/lib/transcribe/index.js +26 -9
  165. package/src/prompts/README.md +3 -1
  166. package/src/prompts/as-object-with-schema.js +3 -8
  167. package/src/prompts/as-schema-org-text.js +10 -2
  168. package/src/prompts/code-features.js +1 -5
  169. package/src/prompts/constants.js +27 -27
  170. package/src/prompts/generate-collection.js +1 -1
  171. package/src/prompts/intent.js +16 -22
  172. package/src/prompts/select-from-threshold.js +1 -2
  173. package/src/prompts/sort.js +4 -8
  174. package/src/prompts/style.js +4 -7
  175. package/src/prompts/wrap-list.js +1 -4
  176. package/src/services/llm-model/global-overrides.spec.js +432 -0
  177. package/src/services/llm-model/index.js +234 -40
  178. package/src/services/llm-model/model.js +2 -2
  179. package/src/services/llm-model/negotiate.spec.js +447 -0
  180. package/src/services/redis/index.js +70 -7
  181. package/src/test/setup.js +20 -0
  182. package/src/verblets/README.md +26 -0
  183. package/src/verblets/auto/index.examples.js +12 -9
  184. package/src/verblets/auto/index.js +10 -10
  185. package/src/verblets/auto/index.spec.js +4 -6
  186. package/src/verblets/bool/README.md +36 -0
  187. package/src/verblets/bool/index.examples.js +53 -1
  188. package/src/verblets/bool/index.js +6 -9
  189. package/src/verblets/bool/index.spec.js +1 -3
  190. package/src/verblets/central-tendency/README.md +166 -0
  191. package/src/verblets/central-tendency/central-tendency-result.json +24 -0
  192. package/src/verblets/central-tendency/index.examples.js +196 -0
  193. package/src/verblets/central-tendency/index.js +171 -0
  194. package/src/verblets/central-tendency/index.spec.js +148 -0
  195. package/src/verblets/enum/index.examples.js +1 -4
  196. package/src/verblets/enum/index.js +7 -4
  197. package/src/verblets/expect/README.md +64 -0
  198. package/src/verblets/expect/index.examples.js +109 -0
  199. package/src/verblets/expect/index.js +75 -0
  200. package/src/verblets/expect/index.spec.js +127 -0
  201. package/src/verblets/intent/index.examples.js +95 -7
  202. package/src/verblets/intent/index.js +56 -68
  203. package/src/verblets/intersection/README.md +16 -0
  204. package/src/verblets/intersection/index.examples.js +89 -0
  205. package/src/verblets/intersection/index.js +84 -0
  206. package/src/verblets/intersection/index.spec.js +60 -0
  207. package/src/verblets/intersection/intersection-result.json +16 -0
  208. package/src/verblets/list-expand/README.md +10 -0
  209. package/src/verblets/list-expand/index.examples.js +14 -0
  210. package/src/verblets/list-expand/index.js +104 -0
  211. package/src/verblets/list-expand/index.spec.js +18 -0
  212. package/src/verblets/list-expand/list-expand-result.json +16 -0
  213. package/src/verblets/list-filter/README.md +22 -0
  214. package/src/verblets/list-filter/index.examples.js +26 -0
  215. package/src/verblets/list-filter/index.js +18 -0
  216. package/src/verblets/list-filter/index.spec.js +19 -0
  217. package/src/verblets/list-find/README.md +11 -0
  218. package/src/verblets/list-find/index.examples.js +15 -0
  219. package/src/verblets/list-find/index.js +17 -0
  220. package/src/verblets/list-find/index.spec.js +19 -0
  221. package/src/verblets/list-group/README.md +16 -0
  222. package/src/verblets/list-group/index.examples.js +16 -0
  223. package/src/verblets/list-group/index.js +112 -0
  224. package/src/verblets/list-group/index.spec.js +35 -0
  225. package/src/verblets/list-group/list-group-result.json +16 -0
  226. package/src/verblets/list-map/README.md +11 -0
  227. package/src/verblets/list-map/index.examples.js +15 -0
  228. package/src/verblets/list-map/index.js +26 -0
  229. package/src/verblets/list-map/index.spec.js +17 -0
  230. package/src/verblets/list-reduce/README.md +10 -0
  231. package/src/verblets/list-reduce/index.examples.js +14 -0
  232. package/src/verblets/list-reduce/index.js +21 -0
  233. package/src/verblets/list-reduce/index.spec.js +27 -0
  234. package/src/verblets/list-reduce/index.spec.jsx +27 -0
  235. package/src/verblets/name/README.md +15 -0
  236. package/src/verblets/name/index.examples.js +28 -0
  237. package/src/verblets/name/index.js +19 -0
  238. package/src/verblets/name/index.spec.js +33 -0
  239. package/src/verblets/name-similar-to/README.md +26 -0
  240. package/src/verblets/name-similar-to/index.examples.js +18 -0
  241. package/src/verblets/name-similar-to/index.js +20 -0
  242. package/src/verblets/name-similar-to/index.spec.js +13 -0
  243. package/src/verblets/number/index.examples.js +173 -7
  244. package/src/verblets/number/index.js +5 -2
  245. package/src/verblets/number/index.spec.js +1 -3
  246. package/src/verblets/number-with-units/index.examples.js +5 -1
  247. package/src/verblets/number-with-units/index.js +74 -9
  248. package/src/verblets/number-with-units/number-with-units-result.json +23 -0
  249. package/src/verblets/schema-org/index.examples.js +2 -7
  250. package/src/verblets/schema-org/index.js +32 -3
  251. package/src/verblets/sentiment/README.md +10 -0
  252. package/src/verblets/sentiment/index.examples.js +20 -0
  253. package/src/verblets/sentiment/index.js +9 -0
  254. package/src/verblets/sentiment/index.spec.js +20 -0
  255. package/src/verblets/to-object/index.js +10 -15
  256. package/src/verblets/to-object/index.spec.js +1 -4
  257. package/.eslintrc.json +0 -42
  258. package/docs/README.md +0 -41
  259. package/docs/babel.config.js +0 -3
  260. package/docs/blog/2019-05-28-first-blog-post.md +0 -12
  261. package/docs/blog/2019-05-29-long-blog-post.md +0 -44
  262. package/docs/blog/2021-08-01-mdx-blog-post.mdx +0 -20
  263. package/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
  264. package/docs/blog/2021-08-26-welcome/index.md +0 -25
  265. package/docs/blog/authors.yml +0 -17
  266. package/docs/docs/api/bool.md +0 -74
  267. package/docs/docs/api/search.md +0 -51
  268. package/docs/docs/intro.md +0 -47
  269. package/docs/docs/tutorial-basics/_category_.json +0 -8
  270. package/docs/docs/tutorial-basics/congratulations.md +0 -23
  271. package/docs/docs/tutorial-basics/create-a-blog-post.md +0 -34
  272. package/docs/docs/tutorial-basics/create-a-document.md +0 -57
  273. package/docs/docs/tutorial-basics/create-a-page.md +0 -43
  274. package/docs/docs/tutorial-basics/deploy-your-site.md +0 -31
  275. package/docs/docs/tutorial-basics/markdown-features.mdx +0 -152
  276. package/docs/docs/tutorial-extras/_category_.json +0 -7
  277. package/docs/docs/tutorial-extras/img/docsVersionDropdown.png +0 -0
  278. package/docs/docs/tutorial-extras/img/localeDropdown.png +0 -0
  279. package/docs/docs/tutorial-extras/manage-docs-versions.md +0 -55
  280. package/docs/docs/tutorial-extras/translate-your-site.md +0 -88
  281. package/docs/docusaurus.config.js +0 -120
  282. package/docs/package.json +0 -44
  283. package/docs/sidebars.js +0 -31
  284. package/docs/src/components/HomepageFeatures/index.js +0 -61
  285. package/docs/src/components/HomepageFeatures/styles.module.css +0 -11
  286. package/docs/src/css/custom.css +0 -30
  287. package/docs/src/pages/index.js +0 -43
  288. package/docs/src/pages/index.module.css +0 -23
  289. package/docs/src/pages/markdown-page.md +0 -7
  290. package/docs/static/.nojekyll +0 -0
  291. package/docs/static/img/docusaurus-social-card.jpg +0 -0
  292. package/docs/static/img/docusaurus.png +0 -0
  293. package/docs/static/img/favicon.ico +0 -0
  294. package/docs/static/img/logo.svg +0 -1
  295. package/docs/static/img/undraw_docusaurus_mountain.svg +0 -171
  296. package/docs/static/img/undraw_docusaurus_react.svg +0 -170
  297. package/docs/static/img/undraw_docusaurus_tree.svg +0 -40
  298. package/src/constants/openai.js +0 -65
  299. /package/{.vite.config.examples.js → .vitest.config.examples.js} +0 -0
  300. /package/{.vite.config.js → .vitest.config.js} +0 -0
@@ -0,0 +1,30 @@
1
+ import listFind from '../../verblets/list-find/index.js';
2
+
3
+ export const bulkFind = async function (list, instructions, config = {}) {
4
+ const { chunkSize = 10, llm, ...options } = config;
5
+ let candidate = '';
6
+ for (let i = 0; i < list.length; i += chunkSize) {
7
+ const batch = list.slice(i, i + chunkSize);
8
+ const combined = candidate ? [candidate, ...batch] : batch;
9
+ // eslint-disable-next-line no-await-in-loop
10
+ candidate = await listFind(combined, instructions, { llm, ...options });
11
+ }
12
+ return candidate;
13
+ };
14
+
15
+ export const bulkFindRetry = async function (list, instructions, config = {}) {
16
+ const { chunkSize = 10, maxAttempts = 3, llm, ...options } = config;
17
+ let result;
18
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
19
+ try {
20
+ // eslint-disable-next-line no-await-in-loop
21
+ result = await bulkFind(list, instructions, { chunkSize, llm, ...options });
22
+ if (result) break;
23
+ } catch {
24
+ // continue
25
+ }
26
+ }
27
+ return result;
28
+ };
29
+
30
+ export default bulkFind;
@@ -0,0 +1,26 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import bulkFind, { bulkFindRetry } from './index.js';
3
+ import listFind from '../../verblets/list-find/index.js';
4
+
5
+ vi.mock('../../verblets/list-find/index.js', () => ({
6
+ default: vi.fn(async (items) => items[items.length - 1]),
7
+ }));
8
+
9
+ beforeEach(() => {
10
+ vi.clearAllMocks();
11
+ });
12
+
13
+ describe('bulk-find chain', () => {
14
+ it('scans batches to find best item', async () => {
15
+ const result = await bulkFind(['a', 'b', 'c', 'd'], 'find', { chunkSize: 2 });
16
+ expect(result).toBe('d');
17
+ expect(listFind).toHaveBeenCalledTimes(2);
18
+ });
19
+
20
+ it('retries on failure', async () => {
21
+ listFind.mockRejectedValueOnce(new Error('fail'));
22
+ const result = await bulkFindRetry(['x', 'y'], 'find', { chunkSize: 2, maxAttempts: 2 });
23
+ expect(result).toBe('y');
24
+ expect(listFind).toHaveBeenCalledTimes(2);
25
+ });
26
+ });
@@ -0,0 +1,23 @@
1
+ # bulk-group
2
+
3
+ Group long lists by first discovering the best categories and then grouping
4
+ items into those categories in smaller batches.
5
+
6
+ ```javascript
7
+ import bulkGroup from './index.js';
8
+
9
+ const feedback = [
10
+ 'Great interface and onboarding',
11
+ 'Price is a bit steep',
12
+ 'Love the mobile app',
13
+ 'Needs more integrations',
14
+ ];
15
+ const result = await bulkGroup(
16
+ feedback,
17
+ 'Is each line praise, criticism, or a feature request?',
18
+ { chunkSize: 2, topN: 3 }
19
+ );
20
+ // => { praise: ['Great interface and onboarding', 'Love the mobile app'],
21
+ // criticism: ['Price is a bit steep'],
22
+ // 'feature request': ['Needs more integrations'] }
23
+ ```
@@ -0,0 +1,18 @@
1
+ import bulkGroup from './index.js';
2
+ import { describe, it, expect } from 'vitest';
3
+ import { longTestTimeout } from '../../constants/common.js';
4
+
5
+ describe('bulk-group examples', () => {
6
+ it(
7
+ 'groups a long list',
8
+ async () => {
9
+ const items = ['dog', 'fish', 'cat', 'whale', 'bird', 'shark', 'horse', 'dolphin'];
10
+ const result = await bulkGroup(items, 'Is each creature terrestrial or aquatic?', {
11
+ chunkSize: 4,
12
+ });
13
+ expect(typeof result).toBe('object');
14
+ expect(Object.keys(result).length).toBeGreaterThan(0);
15
+ },
16
+ longTestTimeout
17
+ );
18
+ });
@@ -0,0 +1,34 @@
1
+ import listGroup from '../../verblets/list-group/index.js';
2
+
3
+ export default async function bulkGroup(list, instructions, config = {}) {
4
+ const { chunkSize = 10, topN, llm, ...options } = config;
5
+ let categories;
6
+ const groups = {};
7
+
8
+ for (let i = 0; i < list.length; i += chunkSize) {
9
+ const batch = list.slice(i, i + chunkSize);
10
+
11
+ // eslint-disable-next-line no-await-in-loop
12
+ const result = await listGroup(batch, instructions, categories, { llm, ...options });
13
+
14
+ // Use categories from first batch for consistency
15
+ if (!categories) {
16
+ categories = Object.keys(result);
17
+ }
18
+
19
+ for (const [key, items] of Object.entries(result)) {
20
+ if (!groups[key]) groups[key] = [];
21
+ groups[key].push(...items);
22
+ }
23
+ }
24
+
25
+ // Apply topN filtering if specified
26
+ if (topN) {
27
+ const sortedEntries = Object.entries(groups)
28
+ .sort(([, a], [, b]) => b.length - a.length)
29
+ .slice(0, topN);
30
+ return Object.fromEntries(sortedEntries);
31
+ }
32
+
33
+ return groups;
34
+ }
@@ -0,0 +1,41 @@
1
+ import bulkGroup from './index.js';
2
+ import listGroup from '../../verblets/list-group/index.js';
3
+ import { vi, describe, it, expect, beforeEach } from 'vitest';
4
+
5
+ vi.mock('../../verblets/list-group/index.js', () => ({
6
+ default: vi.fn(),
7
+ }));
8
+
9
+ describe('bulk-group chain', () => {
10
+ beforeEach(() => {
11
+ vi.clearAllMocks();
12
+ });
13
+
14
+ it('groups in batches', async () => {
15
+ const items = ['a', 'bb', 'ccc', 'dddd', 'eeeee'];
16
+
17
+ // Mock the calls in order - with chunkSize=2, we'll have 3 batches: [a,bb], [ccc,dddd], [eeeee]
18
+ listGroup
19
+ .mockResolvedValueOnce({ odd: ['a'], even: ['bb'] }) // First batch
20
+ .mockResolvedValueOnce({ odd: ['ccc'], even: ['dddd'] }) // Second batch
21
+ .mockResolvedValueOnce({ odd: ['eeeee'] }); // Third batch
22
+
23
+ const result = await bulkGroup(items, 'odd or even', {
24
+ chunkSize: 2,
25
+ });
26
+
27
+ expect(result).toStrictEqual({ odd: ['a', 'ccc', 'eeeee'], even: ['bb', 'dddd'] });
28
+ expect(listGroup).toHaveBeenCalledTimes(3);
29
+
30
+ // Verify the calls were made with the right parameters
31
+ expect(listGroup).toHaveBeenNthCalledWith(1, ['a', 'bb'], 'odd or even', undefined, {
32
+ llm: undefined,
33
+ });
34
+ expect(listGroup).toHaveBeenNthCalledWith(2, ['ccc', 'dddd'], 'odd or even', ['odd', 'even'], {
35
+ llm: undefined,
36
+ });
37
+ expect(listGroup).toHaveBeenNthCalledWith(3, ['eeeee'], 'odd or even', ['odd', 'even'], {
38
+ llm: undefined,
39
+ });
40
+ });
41
+ });
@@ -0,0 +1,43 @@
1
+ # bulk-map
2
+
3
+ Chunk large lists and map each chunk with `listMap`. Failed chunks can be retried.
4
+
5
+ ## Usage
6
+
7
+ ```javascript
8
+ import { bulkMap } from '../../index.js';
9
+
10
+ const films = [
11
+ 'sci-fi epic',
12
+ 'romantic comedy',
13
+ 'time-travel thriller',
14
+ // ...more titles
15
+ ];
16
+ const results = await bulkMap(films, 'Describe each as a Shakespearean play', { chunkSize: 5 });
17
+ // results[0] === 'A saga among the stars'
18
+ // results[1] === 'Where hearts and humor entwine'
19
+ ```
20
+
21
+ ## API
22
+
23
+ ### `bulkMap(list, instructions, [chunkSize])`
24
+
25
+ Break `list` into batches and map each batch using `listMap`.
26
+
27
+ - `list` (`string[]`): fragments to process.
28
+ - `instructions` (`string`): mapping instructions.
29
+ - `chunkSize` (`number`, default `10`): number of items per batch.
30
+
31
+ Returns `Promise<(string|undefined)[]>` where undefined entries represent failed items.
32
+
33
+ ### `bulkMapRetry(list, instructions, [options])`
34
+
35
+ Retry undefined entries from `bulkMap` until `maxAttempts` is reached.
36
+
37
+ - `list` (`string[]`): fragments to process.
38
+ - `instructions` (`string`): mapping instructions.
39
+ - `options.chunkSize` (`number`, default `10`): size of each batch.
40
+ - `options.maxAttempts` (`number`, default `3`): number of passes over failed items.
41
+
42
+ Returns `Promise<(string|undefined)[]>` aligned with input order.
43
+
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import bulkMap from './index.js';
3
+ import { longTestTimeout } from '../../constants/common.js';
4
+
5
+ describe('bulkmap examples', () => {
6
+ it(
7
+ 'maps with listMap',
8
+ async () => {
9
+ const animals = ['dog', 'cat', 'cow', 'sheep', 'duck'];
10
+ const result = await bulkMap(animals, 'Return the sound each animal makes', { chunkSize: 3 });
11
+ // e.g. result[0] === 'bark'
12
+ // result[2] === 'moo'
13
+ expect(result.length).toBe(5);
14
+ },
15
+ longTestTimeout
16
+ );
17
+ });
@@ -0,0 +1,86 @@
1
+ import listMap from '../../verblets/list-map/index.js';
2
+
3
+ /**
4
+ * Map over a list of fragments by calling `listMap` on newline-delimited batches.
5
+ * Missing or mismatched output results in `undefined` entries so callers can
6
+ * selectively retry.
7
+ *
8
+ * @param { string[] } list - array of fragments to process
9
+ * @param { string } instructions - mapping instructions passed to `listMap`
10
+ * @param { object } [config={}] - configuration options
11
+ * @param { number } [config.chunkSize=10] - how many items to send per batch
12
+ * @param { object } [config.llm] - LLM configuration
13
+ * @returns { Promise<(string|undefined)[]> } results aligned with input order
14
+ */
15
+ const bulkMap = async function (list, instructions, config = {}) {
16
+ const { chunkSize = 10, llm, ...options } = config;
17
+ const results = new Array(list.length);
18
+ const promises = [];
19
+
20
+ for (let i = 0; i < list.length; i += chunkSize) {
21
+ const batch = list.slice(i, i + chunkSize);
22
+ const startIndex = i;
23
+
24
+ const p = Promise.resolve()
25
+ .then(() => listMap(batch, instructions, { llm, ...options }))
26
+ .then((output) => {
27
+ if (output.length !== batch.length) {
28
+ for (let j = 0; j < batch.length; j += 1) {
29
+ results[startIndex + j] = undefined;
30
+ }
31
+ return;
32
+ }
33
+ output.forEach((line, j) => {
34
+ results[startIndex + j] = line;
35
+ });
36
+ })
37
+ .catch(() => {
38
+ for (let j = 0; j < batch.length; j += 1) {
39
+ results[startIndex + j] = undefined;
40
+ }
41
+ });
42
+ promises.push(p);
43
+ }
44
+
45
+ await Promise.all(promises);
46
+ return results;
47
+ };
48
+
49
+ /**
50
+ * Retry only the undefined results from `map` until maxAttempts is reached.
51
+ *
52
+ * @param { string[] } list - array of fragments
53
+ * @param { string } instructions - mapping instructions passed to `listMap`
54
+ * @param { object } [config={}] - configuration options
55
+ * @param { number } [config.chunkSize=10]
56
+ * @param { number } [config.maxAttempts=3]
57
+ * @param { object } [config.llm] - LLM configuration
58
+ * @returns { Promise<(string|undefined)[]> }
59
+ */
60
+ export const bulkMapRetry = async function (list, instructions, config = {}) {
61
+ const { chunkSize = 10, maxAttempts = 3, llm, ...options } = config;
62
+ const results = await bulkMap(list, instructions, { chunkSize, llm, ...options });
63
+ for (let attempt = 1; attempt < maxAttempts; attempt += 1) {
64
+ const missingIdx = [];
65
+ const missingFragments = [];
66
+ results.forEach((val, idx) => {
67
+ if (val === undefined) {
68
+ missingIdx.push(idx);
69
+ missingFragments.push(list[idx]);
70
+ }
71
+ });
72
+ if (missingFragments.length === 0) break;
73
+ // eslint-disable-next-line no-await-in-loop
74
+ const retryResults = await bulkMap(missingFragments, instructions, {
75
+ chunkSize,
76
+ llm,
77
+ ...options,
78
+ });
79
+ retryResults.forEach((val, i) => {
80
+ results[missingIdx[i]] = val;
81
+ });
82
+ }
83
+ return results;
84
+ };
85
+
86
+ export default bulkMap;
@@ -0,0 +1,44 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import bulkMap, { bulkMapRetry } from './index.js';
3
+ import listMap from '../../verblets/list-map/index.js';
4
+
5
+ vi.mock('../../verblets/list-map/index.js', () => ({
6
+ default: vi.fn(async (items, instructions) => {
7
+ if (items.includes('FAIL')) throw new Error('fail');
8
+ return items.map((i) => `${i}-${instructions}`);
9
+ }),
10
+ }));
11
+
12
+ beforeEach(() => {
13
+ vi.clearAllMocks();
14
+ });
15
+
16
+ describe('bulkmap', () => {
17
+ it('maps fragments in batches', async () => {
18
+ const result = await bulkMap(['a', 'b', 'c'], 'x', { chunkSize: 2 });
19
+ expect(result).toStrictEqual(['a-x', 'b-x', 'c-x']);
20
+ expect(listMap).toHaveBeenCalledTimes(2);
21
+ });
22
+
23
+ it('leaves undefined on error', async () => {
24
+ listMap.mockRejectedValueOnce(new Error('fail'));
25
+ const result = await bulkMap(['FAIL', 'oops'], 'x', { chunkSize: 2 });
26
+ expect(result).toStrictEqual([undefined, undefined]);
27
+ });
28
+
29
+ it('retries only failed fragments', async () => {
30
+ let call = 0;
31
+ listMap.mockImplementation(async (items) => {
32
+ call += 1;
33
+ if (call === 1) throw new Error('fail');
34
+ return items.map((l) => l.toUpperCase());
35
+ });
36
+
37
+ const result = await bulkMapRetry(['alpha', 'beta'], 'upper', {
38
+ chunkSize: 2,
39
+ maxAttempts: 2,
40
+ });
41
+ expect(result).toStrictEqual(['ALPHA', 'BETA']);
42
+ expect(listMap).toHaveBeenCalledTimes(2);
43
+ });
44
+ });
@@ -0,0 +1,12 @@
1
+ # bulk-reduce
2
+
3
+ Reduce long lists by processing them in smaller batches. Each batch is combined
4
+ with the accumulated result using `listReduce`.
5
+
6
+ ```javascript
7
+ import bulkReduce from './index.js';
8
+
9
+ const logs = ['step one', 'step two', 'step three'];
10
+ const result = await bulkReduce(logs, 'summarize');
11
+ // => 'summary of steps'
12
+ ```
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import bulkReduce from './index.js';
3
+ import { longTestTimeout } from '../../constants/common.js';
4
+
5
+ describe('bulk-reduce examples', () => {
6
+ it(
7
+ 'reduces a long list sequentially',
8
+ async () => {
9
+ const items = ['one', 'two', 'three', 'four'];
10
+ const result = await bulkReduce(items, 'concatenate', { chunkSize: 2 });
11
+ expect(result).toBeDefined();
12
+ },
13
+ longTestTimeout
14
+ );
15
+ });
@@ -0,0 +1,13 @@
1
+ import listReduce from '../../verblets/list-reduce/index.js';
2
+
3
+ export default async function bulkReduce(list, instructions, config = {}) {
4
+ const { chunkSize = 10, initial, llm, ...options } = config;
5
+ let acc = initial;
6
+ for (let i = 0; i < list.length; i += chunkSize) {
7
+ const batch = list.slice(i, i + chunkSize);
8
+
9
+ // eslint-disable-next-line no-await-in-loop
10
+ acc = await listReduce(acc, batch, instructions, { llm, ...options });
11
+ }
12
+ return acc;
13
+ }
@@ -0,0 +1,25 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import bulkReduce from './index.js';
3
+ import listReduce from '../../verblets/list-reduce/index.js';
4
+
5
+ vi.mock('../../verblets/list-reduce/index.js', () => ({
6
+ default: vi.fn(async (acc, list) => [acc, ...list].filter(Boolean).join('-')),
7
+ }));
8
+
9
+ beforeEach(() => {
10
+ vi.clearAllMocks();
11
+ });
12
+
13
+ describe('bulk-reduce chain', () => {
14
+ it('reduces in batches', async () => {
15
+ const result = await bulkReduce(['a', 'b', 'c', 'd'], 'join', { chunkSize: 2 });
16
+ expect(result).toBe('a-b-c-d');
17
+ expect(listReduce).toHaveBeenCalledTimes(2);
18
+ });
19
+
20
+ it('uses initial value', async () => {
21
+ const result = await bulkReduce(['x', 'y'], 'join', { initial: '0', chunkSize: 2 });
22
+ expect(result).toBe('0-x-y');
23
+ expect(listReduce).toHaveBeenCalledTimes(1);
24
+ });
25
+ });
@@ -0,0 +1,16 @@
1
+ # bulk-score
2
+
3
+ Score lines of text on a 0–10 scale with automatic calibration. Each batch returns a JSON array so parsing stays reliable even with long lists. The chain first scores everything, then rescors a few low, middle, and high examples to calibrate. Those references feed a second scoring pass so every item is ranked consistently using OpenAI's JSON schema enforcement.
4
+
5
+ ```javascript
6
+ import bulkScore from './index.js';
7
+
8
+ const slogans = [
9
+ 'Amazing deals every day!',
10
+ 'Unlock a world of wonder',
11
+ 'Buy stuff now',
12
+ ];
13
+
14
+ const { scores } = await bulkScore(slogans, 'How catchy is this marketing slogan?');
15
+ // scores like [6, 9, 2]
16
+ ```
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "type": "object",
4
+ "properties": {
5
+ "scores": {
6
+ "type": "array",
7
+ "description": "Array of numeric scores corresponding to input items",
8
+ "items": {
9
+ "type": "number",
10
+ "minimum": 0,
11
+ "maximum": 10,
12
+ "description": "Score from 0 (worst) to 10 (best)"
13
+ }
14
+ }
15
+ },
16
+ "required": ["scores"],
17
+ "additionalProperties": false
18
+ }
@@ -0,0 +1,22 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { longTestTimeout } from '../../constants/common.js';
3
+ import bulkScore from './index.js';
4
+
5
+ describe('bulkScore examples', () => {
6
+ it(
7
+ 'ranks jokes by humor',
8
+ async () => {
9
+ const jokes = [
10
+ 'Why did the chicken cross the road? To get to the other side!',
11
+ "Parallel lines have so much in common. It's a shame they'll never meet.",
12
+ "I told my computer I needed a break, and it said 'I'll go to sleep.'",
13
+ ];
14
+
15
+ const { scores } = await bulkScore(jokes, 'How funny is this joke?');
16
+
17
+ expect(scores).toHaveLength(jokes.length);
18
+ scores.forEach((s) => expect(typeof s).toBe('number'));
19
+ },
20
+ longTestTimeout
21
+ );
22
+ });
@@ -0,0 +1,133 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import chatGPT from '../../lib/chatgpt/index.js';
5
+ import wrapVariable from '../../prompts/wrap-variable.js';
6
+ import { constants as promptConstants } from '../../prompts/index.js';
7
+
8
+ const { onlyJSONArray } = promptConstants;
9
+
10
+ // Get the directory of this module
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+
14
+ /**
15
+ * Load the JSON schema for bulk score results
16
+ * @returns {Promise<Object>} JSON schema for validation
17
+ */
18
+ async function getBulkScoreSchema() {
19
+ const schemaPath = path.join(__dirname, 'bulk-score-result.json');
20
+ return JSON.parse(await fs.readFile(schemaPath, 'utf8'));
21
+ }
22
+
23
+ /**
24
+ * Create model options for structured outputs
25
+ * @param {string|Object} llm - LLM model name or configuration object
26
+ * @returns {Promise<Object>} Model options for chatGPT
27
+ */
28
+ async function createModelOptions(llm = 'fastGoodCheap') {
29
+ const schema = await getBulkScoreSchema();
30
+
31
+ const responseFormat = {
32
+ type: 'json_schema',
33
+ json_schema: {
34
+ name: 'bulk_score_result',
35
+ schema,
36
+ },
37
+ };
38
+
39
+ if (typeof llm === 'string') {
40
+ return {
41
+ modelName: llm,
42
+ response_format: responseFormat,
43
+ };
44
+ } else {
45
+ return {
46
+ ...llm,
47
+ response_format: responseFormat,
48
+ };
49
+ }
50
+ }
51
+
52
+ async function scoreBatch(items, instructions, reference = [], config = {}) {
53
+ const { llm, ...options } = config;
54
+ const listBlock = wrapVariable(items.join('\n'), { tag: 'items' });
55
+ const refBlock = reference.length
56
+ ? `\nCalibration examples (score - text):\n${wrapVariable(
57
+ reference.map((r) => `${r.score} - ${r.item}`).join('\n'),
58
+ { tag: 'reference' }
59
+ )}`
60
+ : '';
61
+
62
+ const prompt =
63
+ `Score each line in <items> from 0 (worst) to 10 (best) based on: ${instructions}.` +
64
+ `\nRespond with a JSON object containing a "scores" array of numbers in the same order.` +
65
+ `${refBlock}\n${onlyJSONArray}\n${listBlock}`;
66
+
67
+ const modelOptions = await createModelOptions(llm);
68
+ const response = await chatGPT(prompt, {
69
+ modelOptions,
70
+ ...options,
71
+ });
72
+
73
+ // With structured outputs, response should already be parsed and validated
74
+ const parsed = typeof response === 'string' ? JSON.parse(response) : response;
75
+ // Extract scores from the object structure
76
+ const arr = parsed?.scores || parsed;
77
+
78
+ if (!Array.isArray(arr) || arr.length !== items.length) {
79
+ throw new Error('Score batch mismatch');
80
+ }
81
+ return arr.map((n) => Number(n));
82
+ }
83
+
84
+ export default async function bulkScore(list, instructions, config = {}) {
85
+ const { chunkSize = 10, examples, llm, ...options } = config;
86
+ if (!Array.isArray(list) || list.length === 0) {
87
+ return { scores: [], reference: [] };
88
+ }
89
+
90
+ const firstScores = [];
91
+ for (let i = 0; i < list.length; i += chunkSize) {
92
+ // eslint-disable-next-line no-await-in-loop
93
+ const scores = await scoreBatch(list.slice(i, i + chunkSize), instructions, [], {
94
+ llm,
95
+ ...options,
96
+ });
97
+ firstScores.push(...scores);
98
+ }
99
+
100
+ const scored = list.map((item, idx) => ({ item, score: firstScores[idx] }));
101
+
102
+ let reference = examples;
103
+ if (!reference) {
104
+ const valid = scored.filter((s) => Number.isFinite(s.score));
105
+ if (valid.length) {
106
+ valid.sort((a, b) => a.score - b.score);
107
+ const lows = valid.slice(0, 3);
108
+ const highs = valid.slice(-3);
109
+ const midStart = Math.max(0, Math.floor(valid.length / 2) - 1);
110
+ const mids = valid.slice(midStart, midStart + 3);
111
+ reference = [...lows, ...mids, ...highs];
112
+ const refItems = reference.map((r) => r.item);
113
+ const rescored = await scoreBatch(refItems, instructions, [], { llm, ...options });
114
+ rescored.forEach((score, idx) => {
115
+ reference[idx].score = score;
116
+ });
117
+ } else {
118
+ reference = [];
119
+ }
120
+ }
121
+
122
+ const finalScores = [];
123
+ for (let i = 0; i < list.length; i += chunkSize) {
124
+ // eslint-disable-next-line no-await-in-loop
125
+ const scores = await scoreBatch(list.slice(i, i + chunkSize), instructions, reference, {
126
+ llm,
127
+ ...options,
128
+ });
129
+ finalScores.push(...scores);
130
+ }
131
+
132
+ return { scores: finalScores, reference };
133
+ }
@@ -0,0 +1,30 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import bulkScore from './index.js';
3
+ import chatGPT from '../../lib/chatgpt/index.js';
4
+
5
+ vi.mock('../../lib/chatgpt/index.js', () => ({
6
+ default: vi.fn(),
7
+ }));
8
+
9
+ beforeEach(() => {
10
+ vi.clearAllMocks();
11
+ });
12
+
13
+ describe('bulkScore chain', () => {
14
+ it('scores items using two passes', async () => {
15
+ chatGPT
16
+ .mockResolvedValueOnce('[1,2,3]')
17
+ .mockResolvedValueOnce('[1,2,3,4,5,6,7,8,9]')
18
+ .mockResolvedValueOnce('[1,2,3]');
19
+ const { scores, reference } = await bulkScore(['a', 'bb', 'ccc'], 'length');
20
+ expect(scores).toStrictEqual([1, 2, 3]);
21
+ expect(reference.length).toBeGreaterThan(0);
22
+ expect(chatGPT).toHaveBeenCalled();
23
+ });
24
+
25
+ it('uses provided examples', async () => {
26
+ chatGPT.mockResolvedValueOnce('[1]').mockResolvedValueOnce('[1]');
27
+ const { scores } = await bulkScore(['x'], 'length', { examples: [{ item: 'y', score: 2 }] });
28
+ expect(scores[0]).toBe(1);
29
+ });
30
+ });