@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,37 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import searchBestFirst from './index.js';
3
+
4
+ describe('searchBestFirst city walk example', () => {
5
+ it('plans a path across the city', async () => {
6
+ const graph = {
7
+ start: ['museum', 'park'],
8
+ museum: ['cafe', 'gallery'],
9
+ park: ['cafe', 'river'],
10
+ cafe: ['theater'],
11
+ gallery: ['theater'],
12
+ river: ['theater'],
13
+ theater: ['finish'],
14
+ finish: [],
15
+ };
16
+
17
+ const next = ({ node }) => graph[node] || [];
18
+
19
+ const rank = ({ nodes }) => [...nodes].sort(() => 0.5 - Math.random());
20
+
21
+ const visit = ({ node, state }) => ({ ...state, path: [...(state.path || []), node] });
22
+
23
+ const { path } = await searchBestFirst({
24
+ node: 'start',
25
+ next,
26
+ rank,
27
+ visit,
28
+ goal: ({ node }) => node === 'finish',
29
+ state: {},
30
+ returnPath: true,
31
+ });
32
+
33
+ expect(path.length).toBeGreaterThan(2);
34
+ expect(path[0]).toBe('start');
35
+ expect(path[path.length - 1]).toBe('finish');
36
+ });
37
+ });
@@ -4,6 +4,10 @@ const hasOwnToString = (obj) => {
4
4
  return obj.toString !== Object.prototype.toString;
5
5
  };
6
6
 
7
+ const keyFor = (obj) => {
8
+ return hasOwnToString(obj) ? obj.toString() : obj;
9
+ };
10
+
7
11
  const visitDefault = () => {
8
12
  return Promise.reject(new Error('Not Implemented'));
9
13
  };
@@ -17,9 +21,7 @@ const rankDefault = () => {
17
21
  };
18
22
 
19
23
  const filterWith = (state) => (nextNode) => {
20
- return !state.visited.has(
21
- hasOwnToString(nextNode) ? nextNode.toString() : nextNode
22
- );
24
+ return !state.visited.has(keyFor(nextNode));
23
25
  };
24
26
 
25
27
  export default async ({
@@ -28,39 +30,68 @@ export default async ({
28
30
  rank = rankDefault,
29
31
  state: stateInitial = {},
30
32
  visit = visitDefault,
33
+ goal = () => false,
34
+ returnPath = false,
31
35
  }) => {
32
36
  let nodesTodo = [rootNode];
37
+ const parents = new Map();
33
38
  let state = stateInitial;
34
39
  if (!state.visited) {
35
40
  state.visited = new Set();
36
41
  }
42
+ parents.set(keyFor(rootNode), null);
43
+ let lastNode = rootNode;
37
44
 
38
45
  while (nodesTodo.length > 0) {
39
46
  // eslint-disable-next-line no-await-in-loop
40
47
  const nodesRanked = await rank({ nodes: nodesTodo, state });
41
48
  const node = nodesRanked.shift();
49
+ lastNode = node;
42
50
 
43
- const nodesTodoNext = nodesTodo.filter((el) =>
44
- hasOwnToString(node) ? el.toString() !== node.toString() : el !== node
45
- );
51
+ const nodesTodoNext = nodesTodo.filter((el) => keyFor(el) !== keyFor(node));
46
52
 
47
- state.visited.add(hasOwnToString(node) ? node.toString() : node);
53
+ state.visited.add(keyFor(node));
48
54
 
49
55
  // eslint-disable-next-line no-await-in-loop
50
56
  state = await visit({ node, state });
51
57
 
58
+ if (goal({ node, state })) {
59
+ if (returnPath) {
60
+ const path = [];
61
+ for (let cur = node; cur !== null; ) {
62
+ path.unshift(cur);
63
+ cur = parents.get(keyFor(cur));
64
+ }
65
+ return { state, path };
66
+ }
67
+ return state;
68
+ }
69
+
52
70
  // eslint-disable-next-line no-await-in-loop
53
71
  const nextNodes = await next({ node, state });
54
72
 
73
+ nextNodes.filter(filterWith(state)).forEach((nextNode) => {
74
+ const key = keyFor(nextNode);
75
+ if (!parents.has(key)) {
76
+ parents.set(key, node);
77
+ }
78
+ });
79
+
55
80
  nodesTodo = R.unionWith(
56
- (nodeA, nodeB) =>
57
- hasOwnToString(nodeA)
58
- ? nodeA.toString() === nodeB.toString()
59
- : nodeA === nodeB,
81
+ (nodeA, nodeB) => keyFor(nodeA) === keyFor(nodeB),
60
82
  nodesTodoNext,
61
83
  nextNodes.filter(filterWith(state))
62
84
  );
63
85
  }
64
86
 
87
+ if (returnPath) {
88
+ const path = [];
89
+ for (let cur = lastNode; cur !== null; ) {
90
+ path.unshift(cur);
91
+ cur = parents.get(keyFor(cur));
92
+ }
93
+ return { state, path: path.length ? path : null };
94
+ }
95
+
65
96
  return state;
66
97
  };
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import search from './index.js';
3
+
4
+ describe('search-best-first', () => {
5
+ it('returns path to goal', async () => {
6
+ const result = await search({
7
+ node: 0,
8
+ next: ({ node }) => [node + 1],
9
+ rank: ({ nodes }) => nodes,
10
+ visit: ({ node, state }) => ({ ...state, sum: (state.sum || 0) + node }),
11
+ goal: ({ node }) => node >= 3,
12
+ state: {},
13
+ returnPath: true,
14
+ });
15
+
16
+ expect(result.state.sum).toBe(6); // 0 + 1 + 2 + 3
17
+ expect(result.path).toEqual([0, 1, 2, 3]);
18
+ });
19
+
20
+ it('handles missing goal', async () => {
21
+ const result = await search({
22
+ node: 0,
23
+ next: ({ node }) => (node < 2 ? [node + 1] : []),
24
+ rank: ({ nodes }) => nodes,
25
+ // eslint-disable-next-line no-unused-vars
26
+ visit: ({ node, state }) => ({ ...state, count: (state.count || 0) + 1 }),
27
+ goal: ({ node }) => node === 5,
28
+ state: {},
29
+ returnPath: true,
30
+ });
31
+
32
+ expect(result.state.count).toBe(3);
33
+ expect(result.path).toEqual([0, 1, 2]);
34
+ });
35
+ });
@@ -1,6 +1,5 @@
1
- /* eslint-disable no-await-in-loop */
2
- import fs from 'node:fs/promises';
3
- import path from 'node:path';
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
4
3
 
5
4
  import parseJSParts from '../parse-js-parts/index.js';
6
5
  import search from '../search-best-first/index.js';
@@ -11,54 +10,59 @@ export class Node {
11
10
  }
12
11
 
13
12
  toString() {
14
- return `${this.filename}:::${this.functionName ?? ''}`;
13
+ return `${this.filename}:${this.functionName}`;
15
14
  }
16
15
  }
17
16
 
18
17
  const processLocalImport = async (source) => {
19
- const importedFile = await fs.readFile(source, 'utf8');
18
+ const importedFile = await fs.readFile(source, 'utf-8');
20
19
  const parsedImport = parseJSParts(source, importedFile);
21
- return Object.entries(parsedImport.functionsMap).map(
22
- ([importKey, importValue]) => ({
23
- filename: source,
24
- functionName: importValue?.functionName ?? importKey,
25
- functionData: importValue,
26
- })
27
- );
20
+ return Object.entries(parsedImport.functionsMap).map(([importKey, importValue]) => ({
21
+ filename: source,
22
+ functionName: importValue?.functionName ?? importKey,
23
+ functionData: importValue,
24
+ }));
28
25
  };
29
26
 
30
27
  const processNpmImport = async (source, includeNodeModules = false) => {
31
28
  if (!includeNodeModules) return [];
32
29
 
33
30
  try {
34
- const packageJson = JSON.parse(await fs.readFile('./package.json', 'utf8'));
35
- if (
36
- packageJson.dependencies[source] ||
37
- packageJson.devDependencies[source]
31
+ // Find the project root by looking for package.json
32
+ let currentDir = process.cwd();
33
+ let packageJsonPath = path.join(currentDir, 'package.json');
34
+
35
+ // If not found in current directory, try to find it in parent directories
36
+ while (
37
+ !(await fs
38
+ .access(packageJsonPath)
39
+ .then(() => true)
40
+ .catch(() => false))
38
41
  ) {
39
- const nodeModulePath = `./node_modules/${source}`;
42
+ const parentDir = path.dirname(currentDir);
43
+ if (parentDir === currentDir) break; // Reached filesystem root
44
+ currentDir = parentDir;
45
+ packageJsonPath = path.join(currentDir, 'package.json');
46
+ }
47
+
48
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
49
+ if (packageJson.dependencies[source] || packageJson.devDependencies[source]) {
50
+ const nodeModulePath = path.join(currentDir, 'node_modules', source);
40
51
  const npmPackageJson = JSON.parse(
41
- await fs.readFile(`${nodeModulePath}/package.json`, 'utf8')
52
+ await fs.readFile(path.join(nodeModulePath, 'package.json'), 'utf8')
42
53
  );
43
54
  const mainFilePath = npmPackageJson.main || 'index.js';
44
- const importedFile = await fs.readFile(
45
- `${nodeModulePath}/${mainFilePath}`,
46
- 'utf-8'
47
- );
55
+ const importedFile = await fs.readFile(path.join(nodeModulePath, mainFilePath), 'utf-8');
48
56
  const parsedImport = parseJSParts(mainFilePath, importedFile);
49
57
 
50
- return Object.entries(parsedImport.functionsMap).map(
51
- ([importKey, importValue]) => ({
52
- filename: `${nodeModulePath}/${mainFilePath}`,
53
- functionName: importKey,
54
- functionData: importValue,
55
- })
56
- );
58
+ return Object.entries(parsedImport.functionsMap).map(([importKey, importValue]) => ({
59
+ filename: path.join(nodeModulePath, mainFilePath),
60
+ functionName: importKey,
61
+ functionData: importValue,
62
+ }));
57
63
  }
58
64
  } catch (error) {
59
- console.error(
60
- `Process npm import [error]: ${error.message} (source: ${source})`
61
- );
65
+ console.error(`Process npm import [error]: ${error.message} (source: ${source})`);
62
66
  }
63
67
 
64
68
  return [];
@@ -74,9 +78,7 @@ const visitDefault = ({ state }) => {
74
78
  const rank = ({ nodes }) => {
75
79
  // Example: Rank by the length of the function name
76
80
  return nodes.sort(
77
- (a, b) =>
78
- (a.functionName ?? a.filename).length -
79
- (b.functionName ?? b.filename).length
81
+ (a, b) => (a.functionName ?? a.filename).length - (b.functionName ?? b.filename).length
80
82
  );
81
83
  };
82
84
 
@@ -84,14 +86,12 @@ const prepareNext = async ({ node, includeNodeModules }) => {
84
86
  const code = await fs.readFile(node.filename, 'utf-8');
85
87
  const parsed = parseJSParts(node.filename, code);
86
88
 
87
- const functionsFound = Object.entries(parsed.functionsMap).map(
88
- ([, value]) => {
89
- return new Node({
90
- ...value,
91
- filename: node.filename,
92
- });
93
- }
94
- );
89
+ const functionsFound = Object.entries(parsed.functionsMap).map(([, value]) => {
90
+ return new Node({
91
+ ...value,
92
+ filename: node.filename,
93
+ });
94
+ });
95
95
 
96
96
  const importPromises = Object.values(parsed.importsMap).map((importData) => {
97
97
  if (
@@ -99,10 +99,7 @@ const prepareNext = async ({ node, includeNodeModules }) => {
99
99
  importData.source.startsWith('../') ||
100
100
  importData.source.startsWith('/')
101
101
  ) {
102
- const resolvedPath = path.resolve(
103
- path.dirname(node.filename),
104
- importData.source
105
- );
102
+ const resolvedPath = path.resolve(path.dirname(node.filename), importData.source);
106
103
 
107
104
  return processLocalImport(resolvedPath);
108
105
  }
@@ -1,5 +1,3 @@
1
- /* eslint-disable no-await-in-loop */
2
-
3
1
  import path from 'path';
4
2
  import fs from 'fs';
5
3
  import { parse } from 'acorn';
@@ -26,15 +24,12 @@ const flatten = (obj) => {
26
24
  result = Object.keys(obj).reduce((acc, key) => {
27
25
  if (typeof obj[key] === 'object') {
28
26
  const flattenedValue = flatten(obj[key]);
29
- return Object.entries(flattenedValue).reduce(
30
- (innerAcc, [innerKey, innerVal]) => {
31
- return {
32
- ...innerAcc,
33
- [innerKey]: (innerAcc[innerKey] ?? 0) + innerVal,
34
- };
35
- },
36
- acc
37
- );
27
+ return Object.entries(flattenedValue).reduce((innerAcc, [innerKey, innerVal]) => {
28
+ return {
29
+ ...innerAcc,
30
+ [innerKey]: (innerAcc[innerKey] ?? 0) + innerVal,
31
+ };
32
+ }, acc);
38
33
  }
39
34
  return acc;
40
35
  }, result);
@@ -96,9 +91,7 @@ const parseFiles = (files) => {
96
91
  return { ...s.imported }.name; // also has local name
97
92
  });
98
93
  const source = node.source.value;
99
- const importKey = source.startsWith('.')
100
- ? convertImport(file, source)
101
- : source;
94
+ const importKey = source.startsWith('.') ? convertImport(file, source) : source;
102
95
  importsMap[importKey] = {
103
96
  start: node.start,
104
97
  end: node.end,
@@ -207,16 +200,12 @@ const parseFiles = (files) => {
207
200
  const className = node.id.name;
208
201
  node.body.body.forEach((classElement) => {
209
202
  if (classElement.type === 'MethodDefinition') {
210
- functionsSeen[
211
- `${classElement.value.start}:${classElement.value.end}`
212
- ] = true;
203
+ functionsSeen[`${classElement.value.start}:${classElement.value.end}`] = true;
213
204
 
214
- functionsMap[
215
- `MethodDefinition:${className}.${classElement.key.name}`
216
- ] = {
205
+ functionsMap[`MethodDefinition:${className}.${classElement.key.name}`] = {
217
206
  start: node.start,
218
207
  end: node.end,
219
- className: className,
208
+ className,
220
209
  name: classElement.key.name,
221
210
  async: classElement.value.async,
222
211
  generator: classElement.value.generator,
@@ -2,11 +2,7 @@ import modelService from '../../services/llm-model/index.js';
2
2
 
3
3
  export default (
4
4
  text,
5
- {
6
- minCharsToRemove = 10,
7
- model = modelService.getBestAvailableModel(),
8
- targetTokenCount,
9
- }
5
+ { minCharsToRemove = 10, model = modelService.getBestPublicModel(), targetTokenCount }
10
6
  ) => {
11
7
  const ellipsis = '...';
12
8
  const textToTokenRatio = text.length / model.toTokens(text).length;
@@ -21,8 +17,7 @@ export default (
21
17
  const middleIndex = Math.floor(trimmedText.length / 2);
22
18
  const startIndex = middleIndex - Math.ceil(charsToRemove / 2);
23
19
  const endIndex = middleIndex + Math.floor(charsToRemove / 2);
24
- trimmedText =
25
- trimmedText.slice(0, startIndex) + ellipsis + trimmedText.slice(endIndex);
20
+ trimmedText = trimmedText.slice(0, startIndex) + ellipsis + trimmedText.slice(endIndex);
26
21
  tokenCount = model.toTokens(trimmedText).length;
27
22
  }
28
23
 
@@ -59,9 +59,9 @@ describe('Shorten text', () => {
59
59
  expect(example.want.end.test(got)).toBe(true);
60
60
  }
61
61
  if (example.want.maxLength) {
62
- expect(
63
- modelService.getBestAvailableModel().toTokens(got).length
64
- ).toBeLessThanOrEqual(example.want.maxLength);
62
+ expect(modelService.getBestPublicModel().toTokens(got).length).toBeLessThanOrEqual(
63
+ example.want.maxLength
64
+ );
65
65
  }
66
66
  });
67
67
  });
@@ -11,18 +11,13 @@ export default (val) => {
11
11
 
12
12
  const answerPartTrimmed = answerPart.trim() ?? '';
13
13
 
14
- const answerSection = answerPartTrimmed.length
15
- ? answerPart.trim()
16
- : questionPart.trim();
14
+ const answerSection = answerPartTrimmed.length ? answerPart.trim() : questionPart.trim();
17
15
 
18
16
  const answerNoPrefix = answerSection.replace(/[aA]nswer:?/, '').trim();
19
17
 
20
18
  const answerNoPunctuation = answerNoPrefix.replace(/[., ]+$/g, '').trim();
21
19
 
22
- const answerNoQuotes = answerNoPunctuation
23
- .replace(/^['"]/, '')
24
- .replace(/['"]$/, '')
25
- .trim();
20
+ const answerNoQuotes = answerNoPunctuation.replace(/^['"]/, '').replace(/['"]$/, '').trim();
26
21
 
27
22
  if (answerNoQuotes.startsWith('{') || answerNoQuotes.startsWith('[')) {
28
23
  return answerNoQuotes;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Simple template replacement utility
3
+ * Replaces {key} placeholders with values from data object
4
+ *
5
+ * @param {string} template - Template string with {key} placeholders
6
+ * @param {object} data - Data object with key-value pairs
7
+ * @returns {string} - Template with placeholders replaced
8
+ */
9
+ export default function templateReplace(template, data) {
10
+ if (!template || typeof template !== 'string') {
11
+ return template || '';
12
+ }
13
+
14
+ if (data === undefined || data === null || typeof data !== 'object') {
15
+ return template;
16
+ }
17
+
18
+ // Replace all {key} placeholders
19
+ return template.replace(/\{([^}]+)\}/g, (match, key) => {
20
+ const value = data[key];
21
+ return String(value != null ? value : '');
22
+ });
23
+ }
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import templateReplace from './index.js';
3
+
4
+ describe('templateReplace', () => {
5
+ it('replaces single placeholder', () => {
6
+ const result = templateReplace('Hello {name}!', { name: 'World' });
7
+ expect(result).toBe('Hello World!');
8
+ });
9
+
10
+ it('replaces multiple placeholders', () => {
11
+ const result = templateReplace('Hello {name}, you are {age} years old', {
12
+ name: 'John',
13
+ age: 30,
14
+ });
15
+ expect(result).toBe('Hello John, you are 30 years old');
16
+ });
17
+
18
+ it('replaces multiple instances of same placeholder', () => {
19
+ const result = templateReplace('{name} said "{name} is great"', { name: 'Alice' });
20
+ expect(result).toBe('Alice said "Alice is great"');
21
+ });
22
+
23
+ it('handles missing data gracefully', () => {
24
+ const result = templateReplace('Hello {name}!', {});
25
+ expect(result).toBe('Hello !');
26
+ });
27
+
28
+ it('handles null/undefined values', () => {
29
+ const result = templateReplace('Value: {value}', { value: null });
30
+ expect(result).toBe('Value: ');
31
+ });
32
+
33
+ it('handles non-string values', () => {
34
+ const result = templateReplace('Count: {count}, Active: {active}', {
35
+ count: 42,
36
+ active: true,
37
+ });
38
+ expect(result).toBe('Count: 42, Active: true');
39
+ });
40
+
41
+ it('returns original template when no data provided', () => {
42
+ const result = templateReplace('Hello {name}!');
43
+ expect(result).toBe('Hello {name}!');
44
+ });
45
+
46
+ it('returns original template when data is not an object', () => {
47
+ const result = templateReplace('Hello {name}!', 'not an object');
48
+ expect(result).toBe('Hello {name}!');
49
+ });
50
+
51
+ it('handles empty template', () => {
52
+ const result = templateReplace('', { name: 'test' });
53
+ expect(result).toBe('');
54
+ });
55
+
56
+ it('handles template with no placeholders', () => {
57
+ const result = templateReplace('Just plain text', { name: 'test' });
58
+ expect(result).toBe('Just plain text');
59
+ });
60
+ });
@@ -0,0 +1,11 @@
1
+ import stripResponse from '../strip-response/index.js';
2
+
3
+ export default (val) => {
4
+ const clean = stripResponse(String(val)).trim();
5
+ if (clean.toLowerCase() === 'undefined') return undefined;
6
+ const parsed = new Date(clean);
7
+ if (Number.isNaN(parsed.getTime())) {
8
+ throw new Error('ChatGPT output [error]');
9
+ }
10
+ return parsed;
11
+ };
@@ -6,7 +6,7 @@ export default (val) => {
6
6
  if (valLower === 'undefined') return undefined;
7
7
  const valParsed = +stripNumeric(val);
8
8
  if (Number.isNaN(valParsed)) {
9
- throw new Error(`ChatGPT output [error]`);
9
+ throw new Error('ChatGPT output [error]');
10
10
  }
11
11
  return valParsed;
12
12
  };
@@ -1,8 +1,22 @@
1
- import whisper from 'whisper-node';
2
- import record from 'node-record-lpcm16';
1
+ // Lazy import whisper to avoid initialization issues
2
+ let whisper;
3
+ let record;
3
4
 
4
- export default class Transcriber {
5
+ async function getWhisper() {
6
+ if (!whisper) {
7
+ whisper = (await import('whisper-node')).default;
8
+ }
9
+ return whisper;
10
+ }
5
11
 
12
+ async function getRecord() {
13
+ if (!record) {
14
+ record = (await import('node-record-lpcm16')).default;
15
+ }
16
+ return record;
17
+ }
18
+
19
+ export default class Transcriber {
6
20
  constructor(targetWord, silenceDuration = 5000, wordPauseDuration = 2000) {
7
21
  this.targetWord = targetWord;
8
22
  this.silenceDuration = silenceDuration;
@@ -12,8 +26,9 @@ export default class Transcriber {
12
26
  this.recording = null;
13
27
  }
14
28
 
15
- startRecording() {
16
- this.recording = record.record({
29
+ async startRecording() {
30
+ const recordModule = await getRecord();
31
+ this.recording = recordModule.record({
17
32
  sampleRateHertz: 16000,
18
33
  threshold: 0,
19
34
  recordProgram: 'rec',
@@ -30,12 +45,14 @@ export default class Transcriber {
30
45
  }
31
46
  }
32
47
 
33
- transcribe(stream) {
34
- whisper.transcribeStream(stream, { streaming: true })
35
- .then(transcription => {
48
+ async transcribe(stream) {
49
+ const whisperModule = await getWhisper();
50
+ whisperModule
51
+ .transcribeStream(stream, { streaming: true })
52
+ .then((transcription) => {
36
53
  this.handleTranscription(transcription);
37
54
  })
38
- .catch(error => {
55
+ .catch((error) => {
39
56
  console.error(error);
40
57
  });
41
58
  }
@@ -12,4 +12,6 @@ The functions can generate HTML from their inputs and this structure can be tran
12
12
 
13
13
  # Constants
14
14
 
15
- Currently it's reasonable to keep the text fragments needed for building prompts in a single constants.js file. It's forseable that the number of text fragments will grow. At that point, it would make sense to maintain them with a CMS such as Strapi.
15
+ The text snippets used by many prompt functions are collected in
16
+ [`constants.js`](./constants.js). As the set of fragments grows it may make sense
17
+ to manage them with a CMS such as Strapi.
@@ -13,15 +13,10 @@ export default (jsonSchema = jsonSchemaDefault) => {
13
13
  const propertiesJoined = Object.entries(jsonSchema.properties)
14
14
  .map(([key, val]) => {
15
15
  const annotations = Object.entries(val).filter(
16
- ([annKey, annVal]) =>
17
- ['format', 'description'].includes(annKey) && !!annVal
16
+ ([annKey, annVal]) => ['format', 'description'].includes(annKey) && !!annVal
18
17
  );
19
- const annotationsFormatted = annotations
20
- .map(([k, v]) => `${k}: ${v}`)
21
- .join(', ');
22
- const annotationsWrapped = annotations.length
23
- ? ` (${annotationsFormatted})`
24
- : '';
18
+ const annotationsFormatted = annotations.map(([k, v]) => `${k}: ${v}`).join(', ');
19
+ const annotationsWrapped = annotations.length ? ` (${annotationsFormatted})` : '';
25
20
  return `"${key}": "<${val.type ?? ''}${annotationsWrapped}>"`;
26
21
  })
27
22
  .join(', ');
@@ -1,17 +1,25 @@
1
- import { onlyJSON } from './constants.js';
1
+ import { contentIsSchema, onlyJSON } from './constants.js';
2
2
  import asSchemaOrgType from './as-schema-org-type.js';
3
+ import wrapVariable from './wrap-variable.js';
3
4
 
4
5
  const ensureNumbers = 'ensure values meant to be numbers are numbers';
5
6
  const ensureSchemaOrgType = 'ensure the type is a real schema.org type';
6
7
  const ensureProperties = 'ensure the returned object has @context, name';
7
8
 
8
- export default (object, type) => {
9
+ export default (object, type, schema) => {
9
10
  const typeText = `${asSchemaOrgType(type)}`;
11
+ const schemaText = schema
12
+ ? `\n${contentIsSchema} ${wrapVariable(JSON.stringify(schema), {
13
+ tag: 'schema',
14
+ })}`
15
+ : '';
16
+
10
17
  return `Give me "${object}" in schema.org JSON format with a full set of properties. ${
11
18
  typeText ? `${typeText}.` : ''
12
19
  }
13
20
  - ${ensureNumbers}
14
21
  - ${ensureSchemaOrgType}
15
22
  - ${ensureProperties}
23
+ ${schemaText}
16
24
  ${onlyJSON}`;
17
25
  };
@@ -1,8 +1,4 @@
1
- import {
2
- asJSON,
3
- explainAndSeparate,
4
- explainAndSeparateJSON,
5
- } from './constants.js';
1
+ import { asJSON, explainAndSeparate, explainAndSeparateJSON } from './constants.js';
6
2
 
7
3
  export default ({ text, schema }) => {
8
4
  return `Analyze the following code to provide scores for each of the features described below.