ai-functions 0.2.19 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-test.log +105 -0
- package/README.md +232 -37
- package/TODO.md +138 -0
- package/dist/ai-promise.d.ts +219 -0
- package/dist/ai-promise.d.ts.map +1 -0
- package/dist/ai-promise.js +610 -0
- package/dist/ai-promise.js.map +1 -0
- package/dist/ai.d.ts +285 -0
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +842 -0
- package/dist/ai.js.map +1 -0
- package/dist/batch/anthropic.d.ts +23 -0
- package/dist/batch/anthropic.d.ts.map +1 -0
- package/dist/batch/anthropic.js +257 -0
- package/dist/batch/anthropic.js.map +1 -0
- package/dist/batch/bedrock.d.ts +64 -0
- package/dist/batch/bedrock.d.ts.map +1 -0
- package/dist/batch/bedrock.js +586 -0
- package/dist/batch/bedrock.js.map +1 -0
- package/dist/batch/cloudflare.d.ts +37 -0
- package/dist/batch/cloudflare.d.ts.map +1 -0
- package/dist/batch/cloudflare.js +289 -0
- package/dist/batch/cloudflare.js.map +1 -0
- package/dist/batch/google.d.ts +41 -0
- package/dist/batch/google.d.ts.map +1 -0
- package/dist/batch/google.js +360 -0
- package/dist/batch/google.js.map +1 -0
- package/dist/batch/index.d.ts +31 -0
- package/dist/batch/index.d.ts.map +1 -0
- package/dist/batch/index.js +31 -0
- package/dist/batch/index.js.map +1 -0
- package/dist/batch/memory.d.ts +44 -0
- package/dist/batch/memory.d.ts.map +1 -0
- package/dist/batch/memory.js +188 -0
- package/dist/batch/memory.js.map +1 -0
- package/dist/batch/openai.d.ts +37 -0
- package/dist/batch/openai.d.ts.map +1 -0
- package/dist/batch/openai.js +403 -0
- package/dist/batch/openai.js.map +1 -0
- package/dist/batch-map.d.ts +125 -0
- package/dist/batch-map.d.ts.map +1 -0
- package/dist/batch-map.js +406 -0
- package/dist/batch-map.js.map +1 -0
- package/dist/batch-queue.d.ts +273 -0
- package/dist/batch-queue.d.ts.map +1 -0
- package/dist/batch-queue.js +271 -0
- package/dist/batch-queue.js.map +1 -0
- package/dist/context.d.ts +133 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +267 -0
- package/dist/context.js.map +1 -0
- package/dist/embeddings.d.ts +123 -0
- package/dist/embeddings.d.ts.map +1 -0
- package/dist/embeddings.js +170 -0
- package/dist/embeddings.js.map +1 -0
- package/dist/eval/index.d.ts +8 -0
- package/dist/eval/index.d.ts.map +1 -0
- package/dist/eval/index.js +8 -0
- package/dist/eval/index.js.map +1 -0
- package/dist/eval/models.d.ts +66 -0
- package/dist/eval/models.d.ts.map +1 -0
- package/dist/eval/models.js +120 -0
- package/dist/eval/models.js.map +1 -0
- package/dist/eval/runner.d.ts +64 -0
- package/dist/eval/runner.d.ts.map +1 -0
- package/dist/eval/runner.js +148 -0
- package/dist/eval/runner.js.map +1 -0
- package/dist/generate.d.ts +168 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +174 -0
- package/dist/generate.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/index.js.map +1 -0
- package/dist/primitives.d.ts +292 -0
- package/dist/primitives.d.ts.map +1 -0
- package/dist/primitives.js +471 -0
- package/dist/primitives.js.map +1 -0
- package/dist/providers/cloudflare.d.ts +9 -0
- package/dist/providers/cloudflare.d.ts.map +1 -0
- package/dist/providers/cloudflare.js +9 -0
- package/dist/providers/cloudflare.js.map +1 -0
- package/dist/providers/index.d.ts +9 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +9 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/schema.d.ts +54 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +109 -0
- package/dist/schema.js.map +1 -0
- package/dist/template.d.ts +73 -0
- package/dist/template.d.ts.map +1 -0
- package/dist/template.js +129 -0
- package/dist/template.js.map +1 -0
- package/dist/types.d.ts +481 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/evalite.config.ts +19 -0
- package/evals/README.md +212 -0
- package/evals/classification.eval.ts +108 -0
- package/evals/marketing.eval.ts +370 -0
- package/evals/math.eval.ts +94 -0
- package/evals/run-evals.ts +166 -0
- package/evals/structured-output.eval.ts +143 -0
- package/evals/writing.eval.ts +117 -0
- package/examples/batch-blog-posts.ts +160 -0
- package/package.json +59 -43
- package/src/ai-promise.ts +784 -0
- package/src/ai.ts +1183 -0
- package/src/batch/anthropic.ts +375 -0
- package/src/batch/bedrock.ts +801 -0
- package/src/batch/cloudflare.ts +421 -0
- package/src/batch/google.ts +491 -0
- package/src/batch/index.ts +31 -0
- package/src/batch/memory.ts +253 -0
- package/src/batch/openai.ts +557 -0
- package/src/batch-map.ts +534 -0
- package/src/batch-queue.ts +493 -0
- package/src/context.ts +332 -0
- package/src/embeddings.ts +244 -0
- package/src/eval/index.ts +8 -0
- package/src/eval/models.ts +158 -0
- package/src/eval/runner.ts +217 -0
- package/src/generate.ts +245 -0
- package/src/index.ts +154 -0
- package/src/primitives.ts +612 -0
- package/src/providers/cloudflare.ts +15 -0
- package/src/providers/index.ts +14 -0
- package/src/schema.ts +147 -0
- package/src/template.ts +209 -0
- package/src/types.ts +540 -0
- package/test/README.md +105 -0
- package/test/ai-proxy.test.ts +192 -0
- package/test/async-iterators.test.ts +327 -0
- package/test/batch-background.test.ts +482 -0
- package/test/batch-blog-posts.test.ts +387 -0
- package/test/blog-generation.test.ts +510 -0
- package/test/browse-read.test.ts +611 -0
- package/test/core-functions.test.ts +694 -0
- package/test/decide.test.ts +393 -0
- package/test/define.test.ts +274 -0
- package/test/e2e-bedrock-manual.ts +163 -0
- package/test/e2e-bedrock.test.ts +191 -0
- package/test/e2e-flex-gateway.ts +157 -0
- package/test/e2e-flex-manual.ts +183 -0
- package/test/e2e-flex.test.ts +209 -0
- package/test/e2e-google-manual.ts +178 -0
- package/test/e2e-google.test.ts +216 -0
- package/test/embeddings.test.ts +284 -0
- package/test/evals/define-function.eval.test.ts +379 -0
- package/test/evals/primitives.eval.test.ts +384 -0
- package/test/function-types.test.ts +492 -0
- package/test/generate-core.test.ts +319 -0
- package/test/generate.test.ts +163 -0
- package/test/implicit-batch.test.ts +422 -0
- package/test/schema.test.ts +109 -0
- package/test/tagged-templates.test.ts +302 -0
- package/tsconfig.json +8 -6
- package/vitest.config.ts +42 -0
- package/LICENSE +0 -21
- package/db/cache.ts +0 -6
- package/db/mongo.ts +0 -75
- package/dist/mjs/db/cache.d.ts +0 -1
- package/dist/mjs/db/cache.js +0 -5
- package/dist/mjs/db/mongo.d.ts +0 -31
- package/dist/mjs/db/mongo.js +0 -48
- package/dist/mjs/examples/data.d.ts +0 -1105
- package/dist/mjs/examples/data.js +0 -1105
- package/dist/mjs/functions/ai.d.ts +0 -20
- package/dist/mjs/functions/ai.js +0 -83
- package/dist/mjs/functions/ai.test.d.ts +0 -1
- package/dist/mjs/functions/ai.test.js +0 -29
- package/dist/mjs/functions/gpt.d.ts +0 -4
- package/dist/mjs/functions/gpt.js +0 -10
- package/dist/mjs/functions/list.d.ts +0 -7
- package/dist/mjs/functions/list.js +0 -72
- package/dist/mjs/index.d.ts +0 -3
- package/dist/mjs/index.js +0 -3
- package/dist/mjs/queue/kafka.d.ts +0 -0
- package/dist/mjs/queue/kafka.js +0 -1
- package/dist/mjs/queue/memory.d.ts +0 -0
- package/dist/mjs/queue/memory.js +0 -1
- package/dist/mjs/queue/mongo.d.ts +0 -30
- package/dist/mjs/queue/mongo.js +0 -42
- package/dist/mjs/streams/kafka.d.ts +0 -0
- package/dist/mjs/streams/kafka.js +0 -1
- package/dist/mjs/streams/memory.d.ts +0 -0
- package/dist/mjs/streams/memory.js +0 -1
- package/dist/mjs/streams/mongo.d.ts +0 -0
- package/dist/mjs/streams/mongo.js +0 -1
- package/dist/mjs/streams/types.d.ts +0 -0
- package/dist/mjs/streams/types.js +0 -1
- package/dist/mjs/types.d.ts +0 -11
- package/dist/mjs/types.js +0 -1
- package/dist/mjs/utils/completion.d.ts +0 -9
- package/dist/mjs/utils/completion.js +0 -20
- package/dist/mjs/utils/schema.d.ts +0 -10
- package/dist/mjs/utils/schema.js +0 -72
- package/dist/mjs/utils/schema.test.d.ts +0 -1
- package/dist/mjs/utils/schema.test.js +0 -60
- package/dist/mjs/utils/state.d.ts +0 -1
- package/dist/mjs/utils/state.js +0 -19
- package/examples/data.ts +0 -1105
- package/fixup +0 -11
- package/functions/ai.test.ts +0 -41
- package/functions/ai.ts +0 -115
- package/functions/gpt.ts +0 -12
- package/functions/list.ts +0 -84
- package/index.ts +0 -3
- package/queue/kafka.ts +0 -0
- package/queue/memory.ts +0 -0
- package/queue/mongo.ts +0 -88
- package/streams/kafka.ts +0 -0
- package/streams/memory.ts +0 -0
- package/streams/mongo.ts +0 -0
- package/streams/types.ts +0 -0
- package/tsconfig-backup.json +0 -105
- package/tsconfig-base.json +0 -26
- package/tsconfig-cjs.json +0 -8
- package/types.ts +0 -12
- package/utils/completion.ts +0 -28
- package/utils/schema.test.ts +0 -69
- package/utils/schema.ts +0 -74
- package/utils/state.ts +0 -23
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the ai proxy and AI() schema functions
|
|
3
|
+
*
|
|
4
|
+
* These tests use real AI calls via the Cloudflare AI Gateway.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
8
|
+
import { ai, AI, functions, withTemplate } from '../src/index.js'
|
|
9
|
+
|
|
10
|
+
// Skip tests if no gateway configured
|
|
11
|
+
const hasGateway = !!process.env.AI_GATEWAY_URL || !!process.env.ANTHROPIC_API_KEY
|
|
12
|
+
|
|
13
|
+
describe('ai proxy', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
functions.clear()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('exposes functions registry', () => {
|
|
19
|
+
expect(ai.functions).toBeDefined()
|
|
20
|
+
expect(typeof ai.functions.list).toBe('function')
|
|
21
|
+
expect(typeof ai.functions.get).toBe('function')
|
|
22
|
+
expect(typeof ai.functions.set).toBe('function')
|
|
23
|
+
expect(typeof ai.functions.has).toBe('function')
|
|
24
|
+
expect(typeof ai.functions.clear).toBe('function')
|
|
25
|
+
expect(typeof ai.functions.delete).toBe('function')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('exposes define helpers', () => {
|
|
29
|
+
expect(ai.define).toBeDefined()
|
|
30
|
+
expect(typeof ai.define).toBe('function')
|
|
31
|
+
expect(typeof ai.define.generative).toBe('function')
|
|
32
|
+
expect(typeof ai.define.agentic).toBe('function')
|
|
33
|
+
expect(typeof ai.define.human).toBe('function')
|
|
34
|
+
expect(typeof ai.define.code).toBe('function')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('exposes defineFunction', () => {
|
|
38
|
+
expect(typeof ai.defineFunction).toBe('function')
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe.skipIf(!hasGateway)('ai proxy auto-define', () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
functions.clear()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('auto-defines a function on first call', async () => {
|
|
48
|
+
expect(functions.has('greetPerson')).toBe(false)
|
|
49
|
+
|
|
50
|
+
const result = await (ai as Record<string, (args: unknown) => Promise<unknown>>).greetPerson({
|
|
51
|
+
name: 'Alice',
|
|
52
|
+
style: 'friendly',
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
expect(result).toBeDefined()
|
|
56
|
+
expect(functions.has('greetPerson')).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('uses cached definition on second call', async () => {
|
|
60
|
+
// First call - defines the function
|
|
61
|
+
await (ai as Record<string, (args: unknown) => Promise<unknown>>).capitalizeText({
|
|
62
|
+
text: 'hello',
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const fn1 = functions.get('capitalizeText')
|
|
66
|
+
expect(fn1).toBeDefined()
|
|
67
|
+
|
|
68
|
+
// Second call - uses cached definition
|
|
69
|
+
await (ai as Record<string, (args: unknown) => Promise<unknown>>).capitalizeText({
|
|
70
|
+
text: 'world',
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const fn2 = functions.get('capitalizeText')
|
|
74
|
+
expect(fn1).toBe(fn2) // Same cached function
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe.skipIf(!hasGateway)('AI() schema functions', () => {
|
|
79
|
+
it('creates schema-based functions', async () => {
|
|
80
|
+
const client = AI({
|
|
81
|
+
sentiment: {
|
|
82
|
+
sentiment: 'positive | negative | neutral',
|
|
83
|
+
score: 'Confidence score 0-1 (number)',
|
|
84
|
+
explanation: 'Brief explanation',
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
expect(client.sentiment).toBeDefined()
|
|
89
|
+
expect(typeof client.sentiment).toBe('function')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('generates structured output from schema', async () => {
|
|
93
|
+
const client = AI({
|
|
94
|
+
person: {
|
|
95
|
+
name: 'Full name',
|
|
96
|
+
age: 'Age (number)',
|
|
97
|
+
occupation: 'Job title',
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const result = await client.person('A software engineer named Alice who is 30')
|
|
102
|
+
|
|
103
|
+
expect(result).toBeDefined()
|
|
104
|
+
expect(typeof result.name).toBe('string')
|
|
105
|
+
expect(typeof result.age).toBe('number')
|
|
106
|
+
expect(typeof result.occupation).toBe('string')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('generates nested objects', async () => {
|
|
110
|
+
const client = AI({
|
|
111
|
+
profile: {
|
|
112
|
+
user: {
|
|
113
|
+
name: 'Name',
|
|
114
|
+
email: 'Email address',
|
|
115
|
+
},
|
|
116
|
+
preferences: {
|
|
117
|
+
theme: 'light | dark',
|
|
118
|
+
notifications: 'Enabled? (boolean)',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const result = await client.profile('User Alice who prefers dark mode and has notifications on')
|
|
124
|
+
|
|
125
|
+
expect(result).toBeDefined()
|
|
126
|
+
expect(result.user).toBeDefined()
|
|
127
|
+
expect(result.preferences).toBeDefined()
|
|
128
|
+
expect(['light', 'dark']).toContain(result.preferences.theme)
|
|
129
|
+
expect(typeof result.preferences.notifications).toBe('boolean')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('generates arrays', async () => {
|
|
133
|
+
const client = AI({
|
|
134
|
+
todoList: {
|
|
135
|
+
title: 'List title',
|
|
136
|
+
items: ['Todo items'],
|
|
137
|
+
priority: 'high | medium | low',
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const result = await client.todoList('A high priority shopping list with 3 items')
|
|
142
|
+
|
|
143
|
+
expect(result).toBeDefined()
|
|
144
|
+
expect(typeof result.title).toBe('string')
|
|
145
|
+
expect(Array.isArray(result.items)).toBe(true)
|
|
146
|
+
expect(result.items.length).toBeGreaterThan(0)
|
|
147
|
+
expect(['high', 'medium', 'low']).toContain(result.priority)
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe('withTemplate helper', () => {
|
|
152
|
+
it('handles regular function calls', () => {
|
|
153
|
+
const fn = withTemplate((prompt: string) => prompt.toUpperCase())
|
|
154
|
+
|
|
155
|
+
const result = fn('hello world')
|
|
156
|
+
expect(result).toBe('HELLO WORLD')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('handles tagged template literals', () => {
|
|
160
|
+
const fn = withTemplate((prompt: string) => prompt.toUpperCase())
|
|
161
|
+
|
|
162
|
+
const result = fn`hello world`
|
|
163
|
+
expect(result).toBe('HELLO WORLD')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('handles tagged template literals with interpolation', () => {
|
|
167
|
+
const fn = withTemplate((prompt: string) => prompt.toUpperCase())
|
|
168
|
+
|
|
169
|
+
const name = 'Alice'
|
|
170
|
+
const result = fn`hello ${name}!`
|
|
171
|
+
expect(result).toBe('HELLO ALICE!')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('handles multiple interpolations', () => {
|
|
175
|
+
const fn = withTemplate((prompt: string) => prompt)
|
|
176
|
+
|
|
177
|
+
const a = 'one'
|
|
178
|
+
const b = 'two'
|
|
179
|
+
const c = 'three'
|
|
180
|
+
const result = fn`${a}, ${b}, ${c}`
|
|
181
|
+
expect(result).toBe('one, two, three')
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('works with async functions', async () => {
|
|
185
|
+
const fn = withTemplate(async (prompt: string) => {
|
|
186
|
+
return `Result: ${prompt}`
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const result = await fn`async test`
|
|
190
|
+
expect(result).toBe('Result: async test')
|
|
191
|
+
})
|
|
192
|
+
})
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for async iterator support on list and extract
|
|
3
|
+
*
|
|
4
|
+
* Functions that return lists can be streamed with `for await`:
|
|
5
|
+
* - list`...` - streams items as they're generated
|
|
6
|
+
* - extract`...` - streams extracted items
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Mock async generators
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
const mockStreamItems = vi.fn()
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create an async iterable from an array (simulates streaming)
|
|
19
|
+
*/
|
|
20
|
+
async function* createAsyncIterable<T>(items: T[], delayMs = 10): AsyncIterable<T> {
|
|
21
|
+
for (const item of items) {
|
|
22
|
+
await new Promise(resolve => setTimeout(resolve, delayMs))
|
|
23
|
+
yield item
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Mock list function that returns both a promise and an async iterable
|
|
29
|
+
*/
|
|
30
|
+
function createMockStreamingList() {
|
|
31
|
+
return function list(promptOrStrings: string | TemplateStringsArray, ...args: unknown[]) {
|
|
32
|
+
let prompt: string
|
|
33
|
+
|
|
34
|
+
if (Array.isArray(promptOrStrings) && 'raw' in promptOrStrings) {
|
|
35
|
+
prompt = (promptOrStrings as TemplateStringsArray).reduce((acc, str, i) => {
|
|
36
|
+
return acc + str + (args[i] ?? '')
|
|
37
|
+
}, '')
|
|
38
|
+
} else {
|
|
39
|
+
prompt = promptOrStrings as string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const items = mockStreamItems(prompt)
|
|
43
|
+
|
|
44
|
+
// Return an object that is both a Promise and AsyncIterable
|
|
45
|
+
const asyncIterable = createAsyncIterable(items)
|
|
46
|
+
|
|
47
|
+
const result = {
|
|
48
|
+
// Promise interface - resolve to full array
|
|
49
|
+
then: (resolve: (value: string[]) => void, reject?: (error: Error) => void) => {
|
|
50
|
+
return Promise.resolve(items).then(resolve, reject)
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// AsyncIterable interface - stream items
|
|
54
|
+
[Symbol.asyncIterator]: () => asyncIterable[Symbol.asyncIterator](),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return result as Promise<string[]> & AsyncIterable<string>
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Mock extract function with streaming support
|
|
63
|
+
*/
|
|
64
|
+
function createMockStreamingExtract() {
|
|
65
|
+
return function extract(promptOrStrings: string | TemplateStringsArray, ...args: unknown[]) {
|
|
66
|
+
let prompt: string
|
|
67
|
+
|
|
68
|
+
if (Array.isArray(promptOrStrings) && 'raw' in promptOrStrings) {
|
|
69
|
+
prompt = (promptOrStrings as TemplateStringsArray).reduce((acc, str, i) => {
|
|
70
|
+
return acc + str + (args[i] ?? '')
|
|
71
|
+
}, '')
|
|
72
|
+
} else {
|
|
73
|
+
prompt = promptOrStrings as string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const items = mockStreamItems(prompt)
|
|
77
|
+
const asyncIterable = createAsyncIterable(items)
|
|
78
|
+
|
|
79
|
+
const result = {
|
|
80
|
+
then: (resolve: (value: string[]) => void, reject?: (error: Error) => void) => {
|
|
81
|
+
return Promise.resolve(items).then(resolve, reject)
|
|
82
|
+
},
|
|
83
|
+
[Symbol.asyncIterator]: () => asyncIterable[Symbol.asyncIterator](),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result as Promise<string[]> & AsyncIterable<string>
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// list() async iterator tests
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
describe('list() async iteration', () => {
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
mockStreamItems.mockReset()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('can be awaited to get full array', async () => {
|
|
100
|
+
mockStreamItems.mockReturnValue(['Idea 1', 'Idea 2', 'Idea 3'])
|
|
101
|
+
const list = createMockStreamingList()
|
|
102
|
+
|
|
103
|
+
const result = await list`startup ideas`
|
|
104
|
+
|
|
105
|
+
expect(Array.isArray(result)).toBe(true)
|
|
106
|
+
expect(result).toHaveLength(3)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('can be iterated with for await', async () => {
|
|
110
|
+
mockStreamItems.mockReturnValue(['Idea 1', 'Idea 2', 'Idea 3'])
|
|
111
|
+
const list = createMockStreamingList()
|
|
112
|
+
|
|
113
|
+
const collected: string[] = []
|
|
114
|
+
for await (const item of list`startup ideas`) {
|
|
115
|
+
collected.push(item)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
expect(collected).toHaveLength(3)
|
|
119
|
+
expect(collected).toEqual(['Idea 1', 'Idea 2', 'Idea 3'])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('allows early termination with break', async () => {
|
|
123
|
+
mockStreamItems.mockReturnValue(['Idea 1', 'Billion Dollar Idea', 'Idea 3', 'Idea 4', 'Idea 5'])
|
|
124
|
+
const list = createMockStreamingList()
|
|
125
|
+
|
|
126
|
+
const collected: string[] = []
|
|
127
|
+
for await (const idea of list`startup ideas`) {
|
|
128
|
+
collected.push(idea)
|
|
129
|
+
if (idea.includes('Billion')) {
|
|
130
|
+
break
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Should have stopped after finding the billion dollar idea
|
|
135
|
+
expect(collected).toHaveLength(2)
|
|
136
|
+
expect(collected[1]).toBe('Billion Dollar Idea')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('supports nested iteration pattern from README', async () => {
|
|
140
|
+
const marketList = createMockStreamingList()
|
|
141
|
+
const icpList = createMockStreamingList()
|
|
142
|
+
|
|
143
|
+
// Simulate the nested pattern
|
|
144
|
+
mockStreamItems
|
|
145
|
+
.mockReturnValueOnce(['Market A', 'Market B'])
|
|
146
|
+
.mockReturnValueOnce(['ICP 1', 'ICP 2'])
|
|
147
|
+
.mockReturnValueOnce(['ICP 3', 'ICP 4'])
|
|
148
|
+
|
|
149
|
+
const results: Array<{ market: string; icp: string }> = []
|
|
150
|
+
|
|
151
|
+
for await (const market of marketList`market segments`) {
|
|
152
|
+
for await (const icp of icpList`customer profiles for ${market}`) {
|
|
153
|
+
results.push({ market, icp })
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
expect(results).toHaveLength(4)
|
|
158
|
+
expect(results[0]).toEqual({ market: 'Market A', icp: 'ICP 1' })
|
|
159
|
+
expect(results[3]).toEqual({ market: 'Market B', icp: 'ICP 4' })
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('processes items as they stream in', async () => {
|
|
163
|
+
mockStreamItems.mockReturnValue(['Item 1', 'Item 2', 'Item 3'])
|
|
164
|
+
const list = createMockStreamingList()
|
|
165
|
+
|
|
166
|
+
const processedAt: number[] = []
|
|
167
|
+
const startTime = Date.now()
|
|
168
|
+
|
|
169
|
+
for await (const _item of list`items`) {
|
|
170
|
+
processedAt.push(Date.now() - startTime)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Items should be processed incrementally, not all at once
|
|
174
|
+
expect(processedAt[1]).toBeGreaterThan(processedAt[0])
|
|
175
|
+
expect(processedAt[2]).toBeGreaterThan(processedAt[1])
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// ============================================================================
|
|
180
|
+
// extract() async iterator tests
|
|
181
|
+
// ============================================================================
|
|
182
|
+
|
|
183
|
+
describe('extract() async iteration', () => {
|
|
184
|
+
beforeEach(() => {
|
|
185
|
+
mockStreamItems.mockReset()
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('can be awaited to get full array', async () => {
|
|
189
|
+
mockStreamItems.mockReturnValue(['John Smith', 'Jane Doe', 'Bob Wilson'])
|
|
190
|
+
const extract = createMockStreamingExtract()
|
|
191
|
+
|
|
192
|
+
const result = await extract`names from article`
|
|
193
|
+
|
|
194
|
+
expect(Array.isArray(result)).toBe(true)
|
|
195
|
+
expect(result).toHaveLength(3)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('can be iterated with for await', async () => {
|
|
199
|
+
mockStreamItems.mockReturnValue(['email1@example.com', 'email2@example.com'])
|
|
200
|
+
const extract = createMockStreamingExtract()
|
|
201
|
+
|
|
202
|
+
const collected: string[] = []
|
|
203
|
+
for await (const email of extract`email addresses from document`) {
|
|
204
|
+
collected.push(email)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
expect(collected).toHaveLength(2)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('supports the research + extract pattern from README', async () => {
|
|
211
|
+
mockStreamItems.mockReturnValue(['Competitor A', 'Competitor B', 'Competitor C'])
|
|
212
|
+
const extract = createMockStreamingExtract()
|
|
213
|
+
|
|
214
|
+
const competitors: string[] = []
|
|
215
|
+
const marketResearch = 'Report mentioning Competitor A, Competitor B, and Competitor C...'
|
|
216
|
+
|
|
217
|
+
for await (const competitor of extract`company names from ${marketResearch}`) {
|
|
218
|
+
competitors.push(competitor)
|
|
219
|
+
// In real code, you would do: await research`${competitor} vs ${ourProduct}`
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
expect(competitors).toHaveLength(3)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('allows processing each extraction as it completes', async () => {
|
|
226
|
+
mockStreamItems.mockReturnValue(['email1@test.com', 'email2@test.com'])
|
|
227
|
+
const extract = createMockStreamingExtract()
|
|
228
|
+
|
|
229
|
+
const notifications: string[] = []
|
|
230
|
+
for await (const email of extract`emails from document`) {
|
|
231
|
+
// Simulate sending notification
|
|
232
|
+
notifications.push(`Notified: ${email}`)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
expect(notifications).toHaveLength(2)
|
|
236
|
+
expect(notifications[0]).toBe('Notified: email1@test.com')
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
// ============================================================================
|
|
241
|
+
// Combined patterns
|
|
242
|
+
// ============================================================================
|
|
243
|
+
|
|
244
|
+
describe('combined async iteration patterns', () => {
|
|
245
|
+
beforeEach(() => {
|
|
246
|
+
mockStreamItems.mockReset()
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('supports the full market research pattern from README', async () => {
|
|
250
|
+
const list = createMockStreamingList()
|
|
251
|
+
const extract = createMockStreamingExtract()
|
|
252
|
+
|
|
253
|
+
// Mock different results for each call
|
|
254
|
+
mockStreamItems
|
|
255
|
+
.mockReturnValueOnce(['Market A']) // markets
|
|
256
|
+
.mockReturnValueOnce(['ICP 1']) // ICPs for Market A
|
|
257
|
+
.mockReturnValueOnce(['Blog 1', 'Blog 2']) // blog titles
|
|
258
|
+
|
|
259
|
+
const results: string[] = []
|
|
260
|
+
|
|
261
|
+
// Simplified version of README example
|
|
262
|
+
for await (const market of list`market segments for idea`) {
|
|
263
|
+
for await (const icp of list`customer profiles for ${market}`) {
|
|
264
|
+
for await (const title of list`blog titles for ${icp}`) {
|
|
265
|
+
results.push(`${market} > ${icp} > ${title}`)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
expect(results).toHaveLength(2)
|
|
271
|
+
expect(results[0]).toBe('Market A > ICP 1 > Blog 1')
|
|
272
|
+
expect(results[1]).toBe('Market A > ICP 1 > Blog 2')
|
|
273
|
+
})
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
// ============================================================================
|
|
277
|
+
// Type safety
|
|
278
|
+
// ============================================================================
|
|
279
|
+
|
|
280
|
+
describe('async iterator type safety', () => {
|
|
281
|
+
it('list returns string items by default', async () => {
|
|
282
|
+
mockStreamItems.mockReturnValue(['a', 'b', 'c'])
|
|
283
|
+
const list = createMockStreamingList()
|
|
284
|
+
|
|
285
|
+
for await (const item of list`items`) {
|
|
286
|
+
expect(typeof item).toBe('string')
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('extract can return typed objects with schema', async () => {
|
|
291
|
+
// This tests the concept - actual implementation would use schema
|
|
292
|
+
const items = [
|
|
293
|
+
{ name: 'Acme', role: 'competitor' },
|
|
294
|
+
{ name: 'Beta', role: 'partner' },
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
mockStreamItems.mockReturnValue(items)
|
|
298
|
+
const extract = createMockStreamingExtract()
|
|
299
|
+
|
|
300
|
+
const collected: Array<{ name: string; role: string }> = []
|
|
301
|
+
for await (const company of extract`companies from text`) {
|
|
302
|
+
collected.push(company as { name: string; role: string })
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
expect(collected[0]).toHaveProperty('name')
|
|
306
|
+
expect(collected[0]).toHaveProperty('role')
|
|
307
|
+
})
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
// ============================================================================
|
|
311
|
+
// Error handling
|
|
312
|
+
// ============================================================================
|
|
313
|
+
|
|
314
|
+
describe('async iterator error handling', () => {
|
|
315
|
+
it('propagates errors during iteration', async () => {
|
|
316
|
+
mockStreamItems.mockImplementation(() => {
|
|
317
|
+
throw new Error('Generation failed')
|
|
318
|
+
})
|
|
319
|
+
const list = createMockStreamingList()
|
|
320
|
+
|
|
321
|
+
await expect(async () => {
|
|
322
|
+
for await (const _item of list`items`) {
|
|
323
|
+
// Should not reach here
|
|
324
|
+
}
|
|
325
|
+
}).rejects.toThrow('Generation failed')
|
|
326
|
+
})
|
|
327
|
+
})
|