@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.
- package/.cursor/launch.json +30 -0
- package/.cursor/settings.json +20 -0
- package/.github/workflows/branch-protection.yml +22 -0
- package/.github/workflows/ci.yml +120 -0
- package/.prettierrc +6 -0
- package/.release-it.json +4 -1
- package/.vscode/launch.json +31 -0
- package/AGENTS.md +220 -0
- package/DEVELOPING.md +105 -0
- package/README.md +254 -0
- package/eslint.config.js +80 -0
- package/package.json +29 -17
- package/scripts/generate-test/index.js +29 -3
- package/scripts/runner/index.js +26 -0
- package/scripts/simple-editor/index.js +29 -18
- package/scripts/summarize-files/index.js +28 -4
- package/src/chains/README.md +30 -0
- package/src/chains/anonymize/README.md +21 -0
- package/src/chains/anonymize/index.examples.js +75 -0
- package/src/chains/anonymize/index.js +121 -0
- package/src/chains/anonymize/index.spec.js +78 -0
- package/src/chains/bulk-central-tendency/index.examples.js +138 -0
- package/src/chains/bulk-central-tendency/index.js +91 -0
- package/src/chains/bulk-filter/README.md +21 -0
- package/src/chains/bulk-filter/index.examples.js +22 -0
- package/src/chains/bulk-filter/index.js +58 -0
- package/src/chains/bulk-filter/index.spec.js +38 -0
- package/src/chains/bulk-find/README.md +16 -0
- package/src/chains/bulk-find/index.examples.js +20 -0
- package/src/chains/bulk-find/index.js +30 -0
- package/src/chains/bulk-find/index.spec.js +26 -0
- package/src/chains/bulk-group/README.md +23 -0
- package/src/chains/bulk-group/index.examples.js +18 -0
- package/src/chains/bulk-group/index.js +34 -0
- package/src/chains/bulk-group/index.spec.js +41 -0
- package/src/chains/bulk-map/README.md +43 -0
- package/src/chains/bulk-map/index.examples.js +17 -0
- package/src/chains/bulk-map/index.js +86 -0
- package/src/chains/bulk-map/index.spec.js +44 -0
- package/src/chains/bulk-reduce/README.md +12 -0
- package/src/chains/bulk-reduce/index.examples.js +15 -0
- package/src/chains/bulk-reduce/index.js +13 -0
- package/src/chains/bulk-reduce/index.spec.js +25 -0
- package/src/chains/bulk-score/README.md +16 -0
- package/src/chains/bulk-score/bulk-score-result.json +18 -0
- package/src/chains/bulk-score/index.examples.js +22 -0
- package/src/chains/bulk-score/index.js +133 -0
- package/src/chains/bulk-score/index.spec.js +30 -0
- package/src/chains/category-samples/README.md +61 -0
- package/src/chains/category-samples/index.examples.js +103 -0
- package/src/chains/category-samples/index.js +134 -0
- package/src/chains/collect-terms/README.md +12 -0
- package/src/chains/collect-terms/index.examples.js +16 -0
- package/src/chains/collect-terms/index.js +44 -0
- package/src/chains/collect-terms/index.spec.js +25 -0
- package/src/chains/date/README.md +12 -0
- package/src/chains/date/index.examples.js +47 -0
- package/src/chains/date/index.js +74 -0
- package/src/chains/date/index.spec.js +62 -0
- package/src/chains/disambiguate/README.md +22 -0
- package/src/chains/disambiguate/disambiguate-meanings-result.json +16 -0
- package/src/chains/disambiguate/index.examples.js +18 -0
- package/src/chains/disambiguate/index.js +92 -0
- package/src/chains/disambiguate/index.spec.js +25 -0
- package/src/chains/dismantle/README.md +67 -0
- package/src/chains/dismantle/dismantle.examples.js +27 -0
- package/src/chains/dismantle/index.js +6 -17
- package/src/chains/dismantle/index.spec.js +1 -2
- package/src/chains/expect/README.md +171 -0
- package/src/chains/expect/index.examples.js +146 -0
- package/src/chains/expect/index.js +173 -0
- package/src/chains/expect/index.spec.js +324 -0
- package/src/chains/filter-ambiguous/README.md +11 -0
- package/src/chains/filter-ambiguous/index.examples.js +20 -0
- package/src/chains/filter-ambiguous/index.js +49 -0
- package/src/chains/filter-ambiguous/index.spec.js +31 -0
- package/src/chains/glossary/README.md +19 -0
- package/src/chains/glossary/index.examples.js +386 -0
- package/src/chains/glossary/index.js +75 -0
- package/src/chains/glossary/index.spec.js +19 -0
- package/src/chains/intersections/README.md +152 -0
- package/src/chains/intersections/index.examples.js +279 -0
- package/src/chains/intersections/index.js +366 -0
- package/src/chains/intersections/intersection-result.json +38 -0
- package/src/chains/list/index.examples.js +12 -16
- package/src/chains/list/index.js +106 -53
- package/src/chains/list/index.spec.js +8 -9
- package/src/chains/list/list-result.json +16 -0
- package/src/chains/llm-logger/README.md +208 -0
- package/src/chains/llm-logger/index.js +205 -0
- package/src/chains/llm-logger/index.spec.js +330 -0
- package/src/chains/questions/index.examples.js +2 -1
- package/src/chains/questions/index.js +14 -15
- package/src/chains/scan-js/index.js +6 -9
- package/src/chains/set-interval/README.md +81 -0
- package/src/chains/set-interval/index.examples.js +36 -0
- package/src/chains/set-interval/index.js +131 -0
- package/src/chains/set-interval/index.spec.js +70 -0
- package/src/chains/socratic/README.md +17 -0
- package/src/chains/socratic/index.js +64 -0
- package/src/chains/socratic/index.spec.js +24 -0
- package/src/chains/sort/index.examples.js +3 -7
- package/src/chains/sort/index.js +65 -15
- package/src/chains/sort/index.spec.js +5 -8
- package/src/chains/sort/sort-result.json +16 -0
- package/src/chains/summary-map/README.md +9 -1
- package/src/chains/summary-map/index.examples.js +9 -2
- package/src/chains/summary-map/index.js +43 -25
- package/src/chains/summary-map/index.spec.js +78 -3
- package/src/chains/test/index.js +9 -13
- package/src/chains/test-advice/index.js +4 -5
- package/src/chains/themes/README.md +20 -0
- package/src/chains/themes/index.examples.js +17 -0
- package/src/chains/themes/index.js +28 -0
- package/src/chains/themes/index.spec.js +19 -0
- package/src/chains/veiled-variants/index.examples.js +18 -0
- package/src/chains/veiled-variants/index.js +107 -0
- package/src/chains/veiled-variants/index.spec.js +40 -0
- package/src/constants/common.js +0 -2
- package/src/constants/models.js +172 -0
- package/src/index.js +178 -18
- package/src/json-schemas/README.md +13 -0
- package/src/json-schemas/index.js +8 -14
- package/src/json-schemas/schema-dot-org-photograph.json +11 -5
- package/src/json-schemas/schema-dot-org-place.json +78 -5
- package/src/lib/README.md +26 -0
- package/src/lib/bulk-filter/README.md +22 -0
- package/src/lib/bulk-filter/index.examples.js +27 -0
- package/src/lib/bulk-filter/index.js +63 -0
- package/src/lib/bulk-filter/index.spec.js +38 -0
- package/src/lib/bulk-find/README.md +18 -0
- package/src/lib/bulk-find/index.examples.js +19 -0
- package/src/lib/bulk-find/index.js +30 -0
- package/src/lib/bulk-find/index.spec.js +41 -0
- package/src/lib/chatgpt/index.js +63 -43
- package/src/lib/combinations/index.js +30 -0
- package/src/lib/combinations/index.spec.js +23 -0
- package/src/lib/functional/index.js +28 -0
- package/src/lib/logger-service/index.js +32 -0
- package/src/lib/parse-js-parts/index.js +9 -21
- package/src/lib/parse-llm-list/README.md +39 -0
- package/src/lib/parse-llm-list/index.js +54 -0
- package/src/lib/parse-llm-list/index.spec.js +59 -0
- package/src/lib/path-aliases/index.js +1 -3
- package/src/lib/path-aliases/index.spec.js +2 -8
- package/src/lib/pave/index.js +4 -4
- package/src/lib/pave/index.spec.js +6 -3
- package/src/lib/prompt-cache/index.js +14 -10
- package/src/lib/retry/index.js +11 -8
- package/src/lib/ring-buffer/README.md +460 -0
- package/src/lib/ring-buffer/index.js +1074 -0
- package/src/lib/search-best-first/city-walk.spec.js +37 -0
- package/src/lib/search-best-first/index.js +42 -11
- package/src/lib/search-best-first/index.spec.js +35 -0
- package/src/lib/search-js-files/index.js +44 -47
- package/src/lib/search-js-files/scan-file.js +10 -21
- package/src/lib/shorten-text/index.js +2 -7
- package/src/lib/shorten-text/index.spec.js +3 -3
- package/src/lib/strip-response/index.js +2 -7
- package/src/lib/template-replace/index.js +23 -0
- package/src/lib/template-replace/index.spec.js +60 -0
- package/src/lib/to-date/index.js +11 -0
- package/src/lib/to-number/index.js +1 -1
- package/src/lib/transcribe/index.js +26 -9
- package/src/prompts/README.md +3 -1
- package/src/prompts/as-object-with-schema.js +3 -8
- package/src/prompts/as-schema-org-text.js +10 -2
- package/src/prompts/code-features.js +1 -5
- package/src/prompts/constants.js +27 -27
- package/src/prompts/generate-collection.js +1 -1
- package/src/prompts/intent.js +16 -22
- package/src/prompts/select-from-threshold.js +1 -2
- package/src/prompts/sort.js +4 -8
- package/src/prompts/style.js +4 -7
- package/src/prompts/wrap-list.js +1 -4
- package/src/services/llm-model/global-overrides.spec.js +432 -0
- package/src/services/llm-model/index.js +234 -40
- package/src/services/llm-model/model.js +2 -2
- package/src/services/llm-model/negotiate.spec.js +447 -0
- package/src/services/redis/index.js +70 -7
- package/src/test/setup.js +20 -0
- package/src/verblets/README.md +26 -0
- package/src/verblets/auto/index.examples.js +12 -9
- package/src/verblets/auto/index.js +10 -10
- package/src/verblets/auto/index.spec.js +4 -6
- package/src/verblets/bool/README.md +36 -0
- package/src/verblets/bool/index.examples.js +53 -1
- package/src/verblets/bool/index.js +6 -9
- package/src/verblets/bool/index.spec.js +1 -3
- package/src/verblets/central-tendency/README.md +166 -0
- package/src/verblets/central-tendency/central-tendency-result.json +24 -0
- package/src/verblets/central-tendency/index.examples.js +196 -0
- package/src/verblets/central-tendency/index.js +171 -0
- package/src/verblets/central-tendency/index.spec.js +148 -0
- package/src/verblets/enum/index.examples.js +1 -4
- package/src/verblets/enum/index.js +7 -4
- package/src/verblets/expect/README.md +64 -0
- package/src/verblets/expect/index.examples.js +109 -0
- package/src/verblets/expect/index.js +75 -0
- package/src/verblets/expect/index.spec.js +127 -0
- package/src/verblets/intent/index.examples.js +95 -7
- package/src/verblets/intent/index.js +56 -68
- package/src/verblets/intersection/README.md +16 -0
- package/src/verblets/intersection/index.examples.js +89 -0
- package/src/verblets/intersection/index.js +84 -0
- package/src/verblets/intersection/index.spec.js +60 -0
- package/src/verblets/intersection/intersection-result.json +16 -0
- package/src/verblets/list-expand/README.md +10 -0
- package/src/verblets/list-expand/index.examples.js +14 -0
- package/src/verblets/list-expand/index.js +104 -0
- package/src/verblets/list-expand/index.spec.js +18 -0
- package/src/verblets/list-expand/list-expand-result.json +16 -0
- package/src/verblets/list-filter/README.md +22 -0
- package/src/verblets/list-filter/index.examples.js +26 -0
- package/src/verblets/list-filter/index.js +18 -0
- package/src/verblets/list-filter/index.spec.js +19 -0
- package/src/verblets/list-find/README.md +11 -0
- package/src/verblets/list-find/index.examples.js +15 -0
- package/src/verblets/list-find/index.js +17 -0
- package/src/verblets/list-find/index.spec.js +19 -0
- package/src/verblets/list-group/README.md +16 -0
- package/src/verblets/list-group/index.examples.js +16 -0
- package/src/verblets/list-group/index.js +112 -0
- package/src/verblets/list-group/index.spec.js +35 -0
- package/src/verblets/list-group/list-group-result.json +16 -0
- package/src/verblets/list-map/README.md +11 -0
- package/src/verblets/list-map/index.examples.js +15 -0
- package/src/verblets/list-map/index.js +26 -0
- package/src/verblets/list-map/index.spec.js +17 -0
- package/src/verblets/list-reduce/README.md +10 -0
- package/src/verblets/list-reduce/index.examples.js +14 -0
- package/src/verblets/list-reduce/index.js +21 -0
- package/src/verblets/list-reduce/index.spec.js +27 -0
- package/src/verblets/list-reduce/index.spec.jsx +27 -0
- package/src/verblets/name/README.md +15 -0
- package/src/verblets/name/index.examples.js +28 -0
- package/src/verblets/name/index.js +19 -0
- package/src/verblets/name/index.spec.js +33 -0
- package/src/verblets/name-similar-to/README.md +26 -0
- package/src/verblets/name-similar-to/index.examples.js +18 -0
- package/src/verblets/name-similar-to/index.js +20 -0
- package/src/verblets/name-similar-to/index.spec.js +13 -0
- package/src/verblets/number/index.examples.js +173 -7
- package/src/verblets/number/index.js +5 -2
- package/src/verblets/number/index.spec.js +1 -3
- package/src/verblets/number-with-units/index.examples.js +5 -1
- package/src/verblets/number-with-units/index.js +74 -9
- package/src/verblets/number-with-units/number-with-units-result.json +23 -0
- package/src/verblets/schema-org/index.examples.js +2 -7
- package/src/verblets/schema-org/index.js +32 -3
- package/src/verblets/sentiment/README.md +10 -0
- package/src/verblets/sentiment/index.examples.js +20 -0
- package/src/verblets/sentiment/index.js +9 -0
- package/src/verblets/sentiment/index.spec.js +20 -0
- package/src/verblets/to-object/index.js +10 -15
- package/src/verblets/to-object/index.spec.js +1 -4
- package/.eslintrc.json +0 -42
- package/docs/README.md +0 -41
- package/docs/babel.config.js +0 -3
- package/docs/blog/2019-05-28-first-blog-post.md +0 -12
- package/docs/blog/2019-05-29-long-blog-post.md +0 -44
- package/docs/blog/2021-08-01-mdx-blog-post.mdx +0 -20
- package/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
- package/docs/blog/2021-08-26-welcome/index.md +0 -25
- package/docs/blog/authors.yml +0 -17
- package/docs/docs/api/bool.md +0 -74
- package/docs/docs/api/search.md +0 -51
- package/docs/docs/intro.md +0 -47
- package/docs/docs/tutorial-basics/_category_.json +0 -8
- package/docs/docs/tutorial-basics/congratulations.md +0 -23
- package/docs/docs/tutorial-basics/create-a-blog-post.md +0 -34
- package/docs/docs/tutorial-basics/create-a-document.md +0 -57
- package/docs/docs/tutorial-basics/create-a-page.md +0 -43
- package/docs/docs/tutorial-basics/deploy-your-site.md +0 -31
- package/docs/docs/tutorial-basics/markdown-features.mdx +0 -152
- package/docs/docs/tutorial-extras/_category_.json +0 -7
- package/docs/docs/tutorial-extras/img/docsVersionDropdown.png +0 -0
- package/docs/docs/tutorial-extras/img/localeDropdown.png +0 -0
- package/docs/docs/tutorial-extras/manage-docs-versions.md +0 -55
- package/docs/docs/tutorial-extras/translate-your-site.md +0 -88
- package/docs/docusaurus.config.js +0 -120
- package/docs/package.json +0 -44
- package/docs/sidebars.js +0 -31
- package/docs/src/components/HomepageFeatures/index.js +0 -61
- package/docs/src/components/HomepageFeatures/styles.module.css +0 -11
- package/docs/src/css/custom.css +0 -30
- package/docs/src/pages/index.js +0 -43
- package/docs/src/pages/index.module.css +0 -23
- package/docs/src/pages/markdown-page.md +0 -7
- package/docs/static/.nojekyll +0 -0
- package/docs/static/img/docusaurus-social-card.jpg +0 -0
- package/docs/static/img/docusaurus.png +0 -0
- package/docs/static/img/favicon.ico +0 -0
- package/docs/static/img/logo.svg +0 -1
- package/docs/static/img/undraw_docusaurus_mountain.svg +0 -171
- package/docs/static/img/undraw_docusaurus_react.svg +0 -170
- package/docs/static/img/undraw_docusaurus_tree.svg +0 -40
- package/src/constants/openai.js +0 -65
- /package/{.vite.config.examples.js → .vitest.config.examples.js} +0 -0
- /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
|
+
});
|