@far-world-labs/verblets 0.1.1 → 0.1.2

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 +117 -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 +671 -0
  11. package/eslint.config.js +80 -0
  12. package/package.json +28 -16
  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 +3 -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 +21 -41
  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 +4 -4
  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 +11 -16
  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 +84 -1
  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,27 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import bulkFilter from './index.js';
3
+ import { longTestTimeout } from '../../constants/common.js';
4
+
5
+ describe('bulk-filter examples', () => {
6
+ it(
7
+ 'filters items in batches',
8
+ async () => {
9
+ const entries = [
10
+ 'Losing that match taught me the value of persistence.',
11
+ "I hate losing and it proves I'm worthless.",
12
+ 'After failing my exam, I studied harder and passed the retake.',
13
+ "No matter what I do, I'll never succeed.",
14
+ ];
15
+ const result = await bulkFilter(
16
+ entries,
17
+ 'keep only reflections that show personal growth or learning from mistakes',
18
+ 2
19
+ );
20
+ expect(result).toStrictEqual([
21
+ 'Losing that match taught me the value of persistence.',
22
+ 'After failing my exam, I studied harder and passed the retake.',
23
+ ]);
24
+ },
25
+ longTestTimeout
26
+ );
27
+ });
@@ -0,0 +1,63 @@
1
+ import listFilter from '../../verblets/list-filter/index.js';
2
+
3
+ /**
4
+ * Filter a list by processing newline-delimited batches with `listFilter`.
5
+ *
6
+ * @param {string[]} list - array of items to evaluate
7
+ * @param {string} instructions - filter instructions for `listFilter`
8
+ * @param {number} [chunkSize=10] - how many items per batch
9
+ * @returns {Promise<string[]>} filtered items preserving order
10
+ */
11
+ export default async function bulkFilter(list, instructions, chunkSize = 10) {
12
+ const batches = [];
13
+ for (let i = 0; i < list.length; i += chunkSize) {
14
+ const batch = list.slice(i, i + chunkSize);
15
+ const filtered = await listFilter(batch, instructions);
16
+ batches.push(filtered);
17
+ }
18
+ return batches.flat();
19
+ }
20
+
21
+ /**
22
+ * Retry filtering failed batches until `maxAttempts` is reached.
23
+ *
24
+ * @param {string[]} list
25
+ * @param {string} instructions
26
+ * @param {object} [options]
27
+ * @param {number} [options.chunkSize=10]
28
+ * @param {number} [options.maxAttempts=3]
29
+ * @returns {Promise<string[]>}
30
+ */
31
+ export async function bulkFilterRetry(
32
+ list,
33
+ instructions,
34
+ { chunkSize = 10, maxAttempts = 3 } = {}
35
+ ) {
36
+ const batches = [];
37
+ for (let i = 0; i < list.length; i += chunkSize) {
38
+ batches.push(list.slice(i, i + chunkSize));
39
+ }
40
+ const results = new Array(batches.length).fill(null);
41
+ let pending = batches.map((_, idx) => idx);
42
+
43
+ for (let attempt = 0; attempt < maxAttempts && pending.length > 0; attempt += 1) {
44
+ const newPending = [];
45
+ for (const idx of pending) {
46
+ const batch = batches[idx];
47
+ try {
48
+ const filtered = await listFilter(batch, instructions);
49
+ const valid = filtered.every((line) => batch.includes(line));
50
+ if (valid) {
51
+ results[idx] = filtered;
52
+ } else {
53
+ newPending.push(idx);
54
+ }
55
+ } catch {
56
+ newPending.push(idx);
57
+ }
58
+ }
59
+ pending = newPending;
60
+ }
61
+
62
+ return results.flat().filter(Boolean);
63
+ }
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
2
+ import bulkFilter, { bulkFilterRetry } from './index.js';
3
+ import listFilter from '../../verblets/list-filter/index.js';
4
+
5
+ vi.mock('../../verblets/list-filter/index.js', () => ({
6
+ default: vi.fn(async (items, instructions) => {
7
+ if (items.includes('FAIL')) throw new Error('fail');
8
+ return items.filter((l) => l.includes(instructions));
9
+ }),
10
+ }));
11
+
12
+ beforeEach(() => {
13
+ vi.clearAllMocks();
14
+ });
15
+
16
+ describe('bulk-filter', () => {
17
+ it('filters fragments in batches', async () => {
18
+ const result = await bulkFilter(['aa', 'b', 'ca', 'd'], 'a', 2);
19
+ expect(result).toStrictEqual(['aa', 'ca']);
20
+ expect(listFilter).toHaveBeenCalledTimes(2);
21
+ });
22
+
23
+ it('retries failed batches', async () => {
24
+ let call = 0;
25
+ listFilter.mockImplementation(async (items) => {
26
+ call += 1;
27
+ if (call === 1) throw new Error('fail');
28
+ return items.filter((l) => l.includes('a'));
29
+ });
30
+
31
+ const result = await bulkFilterRetry(['aa', 'b', 'ca'], 'a', {
32
+ chunkSize: 2,
33
+ maxAttempts: 2,
34
+ });
35
+ expect(result).toStrictEqual(['aa', 'ca']);
36
+ expect(listFilter).toHaveBeenCalledTimes(3);
37
+ });
38
+ });
@@ -0,0 +1,18 @@
1
+ # bulk-find
2
+
3
+ Search large lists in batches using `listFind` and return the first matching item.
4
+ Failed batches can be retried with `bulkFindRetry`.
5
+
6
+ ```javascript
7
+ import bulkFind, { bulkFindRetry } from './index.js';
8
+
9
+ const diaryEntries = [
10
+ 'Hiked up the mountains today and saw breathtaking views',
11
+ 'Visited the local market and tried a spicy stew',
12
+ 'Spotted penguins playing on the beach this morning'
13
+ ];
14
+ const penguinMoment = await bulkFindRetry(diaryEntries, 'mentions penguins', {
15
+ chunkSize: 2
16
+ });
17
+ // penguinMoment === 'Spotted penguins playing on the beach this morning'
18
+ ```
@@ -0,0 +1,19 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { bulkFindRetry } from './index.js';
3
+ import { longTestTimeout } from '../../constants/common.js';
4
+
5
+ describe('bulk-find examples', () => {
6
+ it(
7
+ 'finds an item across batches',
8
+ async () => {
9
+ const diaryEntries = [
10
+ 'Hiked up the mountains today and saw breathtaking views',
11
+ 'Visited the local market and tried a spicy stew',
12
+ 'Spotted penguins playing on the beach this morning',
13
+ ];
14
+ const result = await bulkFindRetry(diaryEntries, 'mentions penguins');
15
+ expect(result).toBeDefined();
16
+ },
17
+ longTestTimeout
18
+ );
19
+ });
@@ -0,0 +1,30 @@
1
+ import listFind from '../../verblets/list-find/index.js';
2
+
3
+ /**
4
+ * Search a list in batches using `listFind`.
5
+ *
6
+ * @param { string[] } list - array of items to search
7
+ * @param { string } instructions - criteria passed to `listFind`
8
+ * @param { number } [chunkSize=10] - how many items per batch
9
+ * @returns { Promise<string|undefined> } first match or undefined
10
+ */
11
+ export default async function bulkFind(list, instructions, chunkSize = 10) {
12
+ for (let i = 0; i < list.length; i += chunkSize) {
13
+ const batch = list.slice(i, i + chunkSize);
14
+ try {
15
+ const result = await listFind(batch, instructions);
16
+ if (result) return result;
17
+ } catch {
18
+ // ignore and continue to next batch
19
+ }
20
+ }
21
+ return undefined;
22
+ }
23
+
24
+ export async function bulkFindRetry(list, instructions, { chunkSize = 10, maxAttempts = 3 } = {}) {
25
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
26
+ const result = await bulkFind(list, instructions, chunkSize);
27
+ if (result) return result;
28
+ }
29
+ return undefined;
30
+ }
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it, vi, beforeEach } 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 (list, instructions) => list.find((l) => l.includes(instructions)) || ''),
7
+ }));
8
+
9
+ beforeEach(() => {
10
+ vi.clearAllMocks();
11
+ });
12
+
13
+ describe('bulk-find', () => {
14
+ it('returns first match across batches', async () => {
15
+ const result = await bulkFind(['a', 'b', 'c', 'd'], 'c', 2);
16
+ expect(result).toBe('c');
17
+ expect(listFind).toHaveBeenCalledTimes(2);
18
+ });
19
+
20
+ it('returns undefined when not found', async () => {
21
+ listFind.mockResolvedValueOnce('');
22
+ listFind.mockResolvedValueOnce('');
23
+ const result = await bulkFind(['a', 'b'], 'x', 1);
24
+ expect(result).toBeUndefined();
25
+ });
26
+
27
+ it('retries until a match is found', async () => {
28
+ listFind.mockResolvedValueOnce('');
29
+ listFind.mockResolvedValueOnce('c');
30
+ const result = await bulkFindRetry(['a', 'b', 'c'], 'c', { chunkSize: 2, maxAttempts: 2 });
31
+ expect(result).toBe('c');
32
+ expect(listFind).toHaveBeenCalledTimes(2);
33
+ });
34
+
35
+ it('returns undefined after maxAttempts', async () => {
36
+ listFind.mockResolvedValue('');
37
+ const result = await bulkFindRetry(['a', 'b'], 'x', { chunkSize: 1, maxAttempts: 2 });
38
+ expect(result).toBeUndefined();
39
+ expect(listFind).toHaveBeenCalledTimes(4);
40
+ });
41
+ });
@@ -1,18 +1,14 @@
1
1
  import fetch from 'node-fetch';
2
2
 
3
3
  import {
4
- apiKey,
5
- apiUrl,
6
4
  debugPromptGlobally,
7
5
  debugPromptGloballyIfChanged,
8
6
  debugResultGlobally,
9
7
  debugResultGloballyIfChanged,
10
- } from '../../constants/openai.js';
8
+ models,
9
+ } from '../../constants/models.js';
11
10
  import anySignal from '../any-signal/index.js';
12
- import {
13
- get as getPromptResult,
14
- set as setPromptResult,
15
- } from '../prompt-cache/index.js';
11
+ import { get as getPromptResult, set as setPromptResult } from '../prompt-cache/index.js';
16
12
  import TimedAbortController from '../timed-abort-controller/index.js';
17
13
  import modelService from '../../services/llm-model/index.js';
18
14
  import { getClient as getRedis } from '../../services/redis/index.js';
@@ -20,13 +16,11 @@ import { getClient as getRedis } from '../../services/redis/index.js';
20
16
  const shapeOutputDefault = (result) => {
21
17
  // GPT-4
22
18
  if (result.choices[0].message.tool_calls?.length) {
23
- const toolCall = result.choices[0].message.tool_calls[0]
19
+ const toolCall = result.choices[0].message.tool_calls[0];
24
20
  return {
25
- name: toolCall['function'].name,
26
- arguments: JSON.parse(
27
- toolCall['function'].arguments
28
- ),
29
- result: result.choices[0].message.tool_calls[0].function
21
+ name: toolCall.function.name,
22
+ arguments: JSON.parse(toolCall.function.arguments),
23
+ result: result.choices[0].message.tool_calls[0].function,
30
24
  };
31
25
  }
32
26
  if (result.choices[0].message) {
@@ -36,30 +30,30 @@ const shapeOutputDefault = (result) => {
36
30
  };
37
31
 
38
32
  const onBeforeRequestDefault = ({ debugPrompt, isCached, prompt }) => {
39
- if (
40
- debugPrompt ||
41
- debugPromptGlobally ||
42
- (debugPromptGloballyIfChanged && !isCached)
43
- ) {
44
- console.error(`+++ DEBUG PROMPT +++`);
33
+ if (debugPrompt || debugPromptGlobally || (debugPromptGloballyIfChanged && !isCached)) {
34
+ console.error('+++ DEBUG PROMPT +++');
45
35
  console.error(prompt);
46
36
  console.error('+++ DEBUG PROMPT END +++');
47
37
  }
48
38
  };
49
39
 
50
40
  const onAfterRequestDefault = ({ debugResult, isCached, resultShaped }) => {
51
- if (
52
- debugResult ||
53
- debugResultGlobally ||
54
- (debugResultGloballyIfChanged && !isCached)
55
- ) {
56
- console.error(`+++ DEBUG RESULT +++`);
41
+ if (debugResult || debugResultGlobally || (debugResultGloballyIfChanged && !isCached)) {
42
+ console.error('+++ DEBUG RESULT +++');
57
43
  console.error(resultShaped);
58
44
  console.error('+++ DEBUG RESULT END +++');
59
45
  }
60
46
  };
61
47
 
62
- export const run = async (prompt, options = {}) => {
48
+ export const run = async (prompt, config = {}) => {
49
+ // Handle config parameter - can be string (model name) or object (full options)
50
+ let options;
51
+ if (typeof config === 'string') {
52
+ options = { modelOptions: { modelName: config } };
53
+ } else {
54
+ options = config;
55
+ }
56
+
63
57
  const {
64
58
  abortSignal,
65
59
  debugPrompt,
@@ -71,17 +65,43 @@ export const run = async (prompt, options = {}) => {
71
65
  shapeOutput = shapeOutputDefault,
72
66
  } = options;
73
67
 
74
- debugger;
68
+ // Apply global overrides to model options
69
+ const modelOptionsWithOverrides = modelService.applyGlobalOverrides(modelOptions);
70
+
71
+ // Check if negotiation was applied via global override
72
+ const negotiationFromGlobalOverride = modelService.getGlobalOverride('negotiate');
73
+
74
+ const modelNameNegotiated = modelOptionsWithOverrides.negotiate
75
+ ? modelService.negotiateModel(
76
+ // If negotiation came from global override, don't use preferred model
77
+ negotiationFromGlobalOverride ? null : modelOptionsWithOverrides.modelName,
78
+ modelOptionsWithOverrides.negotiate
79
+ )
80
+ : modelOptionsWithOverrides.modelName;
81
+
82
+ const modelFound = modelService.getModel(modelNameNegotiated);
75
83
 
76
- const modelFound = modelService.getModel(modelOptions.modelName);
84
+ // Use model-specific API URL and key if defined, otherwise fall back to defaults
85
+ const apiUrl = modelFound?.apiUrl || models.fastGood.apiUrl;
86
+ const apiKey = modelFound?.apiKey || models.fastGood.apiKey;
77
87
 
78
88
  const requestConfig = modelService.getRequestConfig({
79
89
  prompt,
80
- ...modelOptions,
90
+ ...modelOptionsWithOverrides,
91
+ modelName: modelNameNegotiated,
81
92
  });
82
93
 
83
- const cache = await getRedis();
84
- const { result: cacheResult } = await getPromptResult(cache, requestConfig);
94
+ // Check if caching is disabled via environment variable
95
+ const cachingDisabled = process.env.DISABLE_CACHE === 'true';
96
+
97
+ let cacheResult = null;
98
+ let cache = null;
99
+
100
+ if (!cachingDisabled) {
101
+ cache = await getRedis();
102
+ const { result } = await getPromptResult(cache, requestConfig);
103
+ cacheResult = result;
104
+ }
85
105
 
86
106
  onBeforeRequest({
87
107
  isCached: !!cacheResult,
@@ -92,9 +112,12 @@ export const run = async (prompt, options = {}) => {
92
112
 
93
113
  let result = cacheResult;
94
114
  if (!cacheResult || forceQuery) {
95
- const timeoutController = new TimedAbortController(
96
- modelService.getModel(modelOptions.modelName).requestTimeout
97
- );
115
+ // Use custom requestTimeout from modelOptions if provided, otherwise use model default
116
+ const requestTimeout =
117
+ modelOptionsWithOverrides.requestTimeout ||
118
+ modelService.getModel(modelNameNegotiated).requestTimeout;
119
+
120
+ const timeoutController = new TimedAbortController(requestTimeout);
98
121
 
99
122
  // console.log(requestConfig, `${apiUrl}${modelFound.endpoint}`)
100
123
 
@@ -111,21 +134,18 @@ export const run = async (prompt, options = {}) => {
111
134
  result = await response.json();
112
135
 
113
136
  if (!response.ok) {
114
- const vars = [
115
- `status: ${response?.status}`,
116
- `type: ${result?.error?.type}`,
117
- ].join(', ');
118
- throw new Error(
119
- `Completions request [error]: ${result?.error?.message} (${vars})`
120
- );
137
+ const vars = [`status: ${response?.status}`, `type: ${result?.error?.type}`].join(', ');
138
+ throw new Error(`Completions request [error]: ${result?.error?.message} (${vars})`);
121
139
  }
122
140
 
123
141
  timeoutController.clearTimeout();
124
142
 
125
- await setPromptResult(cache, requestConfig, result);
143
+ // Only cache the result if caching is not disabled
144
+ if (!cachingDisabled && cache) {
145
+ await setPromptResult(cache, requestConfig, result);
146
+ }
126
147
  }
127
148
 
128
- debugger;
129
149
  const resultShaped = shapeOutput(result);
130
150
 
131
151
  onAfterRequest({
@@ -0,0 +1,30 @@
1
+ export default function combinations(items, size = 2) {
2
+ const result = [];
3
+ if (!Array.isArray(items) || size < 1) return result;
4
+
5
+ const combo = [];
6
+ const generate = (start) => {
7
+ if (combo.length === size) {
8
+ result.push([...combo]);
9
+ return;
10
+ }
11
+ for (let i = start; i < items.length; i++) {
12
+ combo.push(items[i]);
13
+ generate(i + 1);
14
+ combo.pop();
15
+ }
16
+ };
17
+
18
+ generate(0);
19
+ return result;
20
+ }
21
+
22
+ export function rangeCombinations(items, minSize = 2, maxSize = items.length) {
23
+ const sets = [];
24
+ if (!Array.isArray(items)) return sets;
25
+ const upper = Math.min(items.length, maxSize);
26
+ for (let s = minSize; s <= upper; s++) {
27
+ sets.push(...combinations(items, s));
28
+ }
29
+ return sets;
30
+ }
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import combinations, { rangeCombinations } from './index.js';
3
+
4
+ describe('combinations helper', () => {
5
+ it('generates pairwise combinations', () => {
6
+ const result = combinations(['a', 'b', 'c'], 2);
7
+ expect(result).toStrictEqual([
8
+ ['a', 'b'],
9
+ ['a', 'c'],
10
+ ['b', 'c'],
11
+ ]);
12
+ });
13
+
14
+ it('generates combinations of varying sizes', () => {
15
+ const result = rangeCombinations(['a', 'b', 'c']);
16
+ expect(result).toStrictEqual([
17
+ ['a', 'b'],
18
+ ['a', 'c'],
19
+ ['b', 'c'],
20
+ ['a', 'b', 'c'],
21
+ ]);
22
+ });
23
+ });
@@ -0,0 +1,28 @@
1
+ export const hook = (list, f, returnVal) => {
2
+ list.push(f);
3
+ return returnVal;
4
+ };
5
+
6
+ export const hookOnce = (list, f, returnVal) => {
7
+ if (list.includes(f)) return returnVal;
8
+ list.push(f);
9
+ return returnVal;
10
+ };
11
+
12
+ export const hookFn = (list, returnVal) => (f) => hook(list, f, returnVal);
13
+
14
+ export const hookOnceFn = (list, returnVal) => (f) => hookOnce(list, f, returnVal);
15
+
16
+ export const unhook = (list, f, returnVal) => {
17
+ const idx = list.indexOf(f);
18
+ if (idx === -1) return returnVal;
19
+ list.splice(idx, 1);
20
+ return returnVal;
21
+ };
22
+
23
+ export const unhookFn = (list, returnVal) => (f) => unhook(list, f, returnVal);
24
+
25
+ export const unhookAll = (list, returnVal) => {
26
+ list.splice(0);
27
+ return returnVal;
28
+ };
@@ -0,0 +1,32 @@
1
+ // Global logger service
2
+ let globalLogger = null;
3
+
4
+ /**
5
+ * Set the global logger instance
6
+ * @param {Object} logger - Logger instance to use globally
7
+ */
8
+ export const setLogger = (logger) => {
9
+ globalLogger = logger;
10
+ };
11
+
12
+ /**
13
+ * Reset the global logger to null
14
+ */
15
+ export const resetLogger = () => {
16
+ globalLogger = null;
17
+ };
18
+
19
+ /**
20
+ * Get the current global logger instance
21
+ * @returns {Object|null} Current logger instance or null if not set
22
+ */
23
+ export const getLogger = () => globalLogger;
24
+
25
+ // Export convenience methods that use the global logger
26
+ export const log = (...args) => globalLogger?.log(...args);
27
+ export const info = (...args) => globalLogger?.info(...args);
28
+ export const warn = (...args) => globalLogger?.warn(...args);
29
+ export const error = (...args) => globalLogger?.error(...args);
30
+ export const debug = (...args) => globalLogger?.debug(...args);
31
+ export const trace = (...args) => globalLogger?.trace(...args);
32
+ export const fatal = (...args) => globalLogger?.fatal(...args);
@@ -44,9 +44,7 @@ const scanFile = (file, code) => {
44
44
  return { ...s.imported }.name; // also has local name
45
45
  });
46
46
  const source = importNode.source.value;
47
- const importKey = source.startsWith('.')
48
- ? convertImport(file, source)
49
- : source;
47
+ const importKey = source.startsWith('.') ? convertImport(file, source) : source;
50
48
  importsMap[importKey] = {
51
49
  start: importNode.start,
52
50
  end: importNode.end,
@@ -60,9 +58,7 @@ const scanFile = (file, code) => {
60
58
  if (expNode.source) {
61
59
  // Handle re-exports
62
60
  const source = expNode.source.value;
63
- const importKey = source.startsWith('.')
64
- ? convertImport(file, source)
65
- : source;
61
+ const importKey = source.startsWith('.') ? convertImport(file, source) : source;
66
62
 
67
63
  // Named exports
68
64
  if (expNode.specifiers) {
@@ -128,9 +124,7 @@ const scanFile = (file, code) => {
128
124
  ExportAllDeclaration(expNode) {
129
125
  if (expNode.source) {
130
126
  const source = expNode.source.value;
131
- const importKey = source.startsWith('.')
132
- ? convertImport(file, source)
133
- : source;
127
+ const importKey = source.startsWith('.') ? convertImport(file, source) : source;
134
128
  exportsMap[expNode.exported.name] = {
135
129
  start: expNode.start,
136
130
  end: expNode.end,
@@ -161,7 +155,7 @@ const scanFile = (file, code) => {
161
155
  end: expNode.end,
162
156
  };
163
157
 
164
- functionsMap[`ArrowFunctionExpression:default`] = {
158
+ functionsMap['ArrowFunctionExpression:default'] = {
165
159
  start: expNode.declaration.start,
166
160
  end: expNode.declaration.end,
167
161
  functionName: '<default-export>',
@@ -195,9 +189,7 @@ const scanFile = (file, code) => {
195
189
  });
196
190
 
197
191
  arrowDeclarations.forEach((arrowFnNode) => {
198
- functionsSeen[
199
- `${arrowFnNode.init.start}:${arrowFnNode.init.end}`
200
- ] = true;
192
+ functionsSeen[`${arrowFnNode.init.start}:${arrowFnNode.init.end}`] = true;
201
193
 
202
194
  functionsMap[`ArrowFunctionExpression:${arrowFnNode.id.name}`] = {
203
195
  start: arrowFnNode.start,
@@ -283,16 +275,12 @@ const scanFile = (file, code) => {
283
275
  const className = node.id.name;
284
276
  node.body.body.forEach((classElement) => {
285
277
  if (classElement.type === 'MethodDefinition') {
286
- functionsSeen[
287
- `${classElement.value.start}:${classElement.value.end}`
288
- ] = true;
278
+ functionsSeen[`${classElement.value.start}:${classElement.value.end}`] = true;
289
279
 
290
- functionsMap[
291
- `MethodDefinition:${className}.${classElement.key.name}`
292
- ] = {
280
+ functionsMap[`MethodDefinition:${className}.${classElement.key.name}`] = {
293
281
  start: node.start,
294
282
  end: node.end,
295
- className: className,
283
+ className,
296
284
  functionName: `${className}.${classElement.key.name}`,
297
285
  name: classElement.key.name,
298
286
  async: classElement.value.async,
@@ -310,7 +298,7 @@ const scanFile = (file, code) => {
310
298
  if (!functionsSeen[`${node.start}:${node.end}`]) {
311
299
  functionsMap[`${node.type}:${node.start}`] = {
312
300
  start: node.start,
313
- functionName: `<exp>`,
301
+ functionName: '<exp>',
314
302
  end: node.end,
315
303
  type: node.type,
316
304
  async: node.async,
@@ -0,0 +1,39 @@
1
+ # Parse LLM List
2
+
3
+ A utility function for parsing lists from LLM responses that may be in JSON array or CSV format.
4
+
5
+ ## Usage
6
+
7
+ ```javascript
8
+ import parseLLMList from './src/lib/parse-llm-list/index.js';
9
+
10
+ // Parse JSON array response
11
+ const jsonResponse = '["term1", "term2", "term3"]';
12
+ const terms1 = parseLLMList(jsonResponse);
13
+ // Returns: ["term1", "term2", "term3"]
14
+
15
+ // Parse CSV response
16
+ const csvResponse = 'term1, term2, term3';
17
+ const terms2 = parseLLMList(csvResponse);
18
+ // Returns: ["term1", "term2", "term3"]
19
+
20
+ // With custom options
21
+ const terms3 = parseLLMList(response, {
22
+ excludeValues: ['none', 'null', 'n/a'],
23
+ trimItems: true,
24
+ filterEmpty: true
25
+ });
26
+ ```
27
+
28
+ ## Options
29
+
30
+ - `excludeValues` (string[]): Values to exclude from results (case-insensitive). Default: `['none', 'null', 'undefined']`
31
+ - `trimItems` (boolean): Whether to trim whitespace from items. Default: `true`
32
+ - `filterEmpty` (boolean): Whether to filter out empty strings. Default: `true`
33
+
34
+ ## Features
35
+
36
+ - Automatically detects JSON array vs CSV format
37
+ - Handles common LLM response patterns like `<note>` tags
38
+ - Filters out excluded values and empty responses
39
+ - Robust error handling with fallback parsing