ai-functions 2.0.2 → 2.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/.turbo/turbo-build.log +4 -5
- package/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.md +361 -159
- package/dist/ai-promise.d.ts +47 -0
- package/dist/ai-promise.d.ts.map +1 -1
- package/dist/ai-promise.js +291 -3
- package/dist/ai-promise.js.map +1 -1
- package/dist/ai.d.ts +17 -18
- package/dist/ai.d.ts.map +1 -1
- package/dist/ai.js +93 -39
- package/dist/ai.js.map +1 -1
- package/dist/batch-map.d.ts +46 -4
- package/dist/batch-map.d.ts.map +1 -1
- package/dist/batch-map.js +35 -2
- package/dist/batch-map.js.map +1 -1
- package/dist/batch-queue.d.ts +116 -12
- package/dist/batch-queue.d.ts.map +1 -1
- package/dist/batch-queue.js +47 -2
- package/dist/batch-queue.js.map +1 -1
- package/dist/budget.d.ts +272 -0
- package/dist/budget.d.ts.map +1 -0
- package/dist/budget.js +500 -0
- package/dist/budget.js.map +1 -0
- package/dist/cache.d.ts +272 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +412 -0
- package/dist/cache.js.map +1 -0
- package/dist/context.d.ts +32 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +16 -1
- package/dist/context.js.map +1 -1
- package/dist/eval/runner.d.ts +2 -1
- package/dist/eval/runner.d.ts.map +1 -1
- package/dist/eval/runner.js.map +1 -1
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +6 -10
- package/dist/generate.js.map +1 -1
- package/dist/index.d.ts +27 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +72 -42
- package/dist/index.js.map +1 -1
- package/dist/primitives.d.ts +17 -0
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +19 -1
- package/dist/primitives.js.map +1 -1
- package/dist/retry.d.ts +303 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +539 -0
- package/dist/retry.js.map +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +1 -9
- package/dist/schema.js.map +1 -1
- package/dist/tool-orchestration.d.ts +391 -0
- package/dist/tool-orchestration.d.ts.map +1 -0
- package/dist/tool-orchestration.js +663 -0
- package/dist/tool-orchestration.js.map +1 -0
- package/dist/types.d.ts +50 -33
- package/dist/types.d.ts.map +1 -1
- package/evalite.config.js +14 -0
- package/evals/classification.eval.js +97 -0
- package/evals/marketing.eval.js +289 -0
- package/evals/math.eval.js +83 -0
- package/evals/run-evals.js +151 -0
- package/evals/structured-output.eval.js +131 -0
- package/evals/writing.eval.js +105 -0
- package/examples/batch-blog-posts.js +128 -0
- package/package.json +26 -26
- package/src/ai-promise.ts +359 -3
- package/src/ai.ts +155 -110
- package/src/batch/anthropic.js +256 -0
- package/src/batch/bedrock.js +584 -0
- package/src/batch/cloudflare.js +287 -0
- package/src/batch/google.js +359 -0
- package/src/batch/index.js +30 -0
- package/src/batch/memory.js +187 -0
- package/src/batch/openai.js +402 -0
- package/src/batch-map.ts +46 -4
- package/src/batch-queue.ts +116 -12
- package/src/budget.ts +727 -0
- package/src/cache.ts +653 -0
- package/src/context.ts +33 -1
- package/src/eval/index.js +7 -0
- package/src/eval/models.js +119 -0
- package/src/eval/runner.js +147 -0
- package/src/eval/runner.ts +3 -2
- package/src/generate.ts +7 -12
- package/src/index.ts +231 -53
- package/src/primitives.ts +19 -1
- package/src/retry.ts +776 -0
- package/src/schema.ts +1 -10
- package/src/tool-orchestration.ts +1008 -0
- package/src/types.ts +59 -41
- package/test/ai-proxy.test.js +157 -0
- package/test/async-iterators.test.js +261 -0
- package/test/backward-compat.test.ts +147 -0
- package/test/batch-autosubmit-errors.test.ts +598 -0
- package/test/batch-background.test.js +352 -0
- package/test/batch-blog-posts.test.js +293 -0
- package/test/blog-generation.test.js +390 -0
- package/test/browse-read.test.js +480 -0
- package/test/budget-tracking.test.ts +800 -0
- package/test/cache.test.ts +712 -0
- package/test/context-isolation.test.ts +687 -0
- package/test/core-functions.test.js +490 -0
- package/test/decide.test.js +260 -0
- package/test/define.test.js +232 -0
- package/test/e2e-bedrock-manual.js +136 -0
- package/test/e2e-bedrock.test.js +164 -0
- package/test/e2e-flex-gateway.js +131 -0
- package/test/e2e-flex-manual.js +156 -0
- package/test/e2e-flex.test.js +174 -0
- package/test/e2e-google-manual.js +150 -0
- package/test/e2e-google.test.js +181 -0
- package/test/embeddings.test.js +220 -0
- package/test/evals/define-function.eval.test.js +309 -0
- package/test/evals/deterministic.eval.test.ts +376 -0
- package/test/evals/primitives.eval.test.js +360 -0
- package/test/function-types.test.js +407 -0
- package/test/generate-core.test.js +213 -0
- package/test/generate.test.js +143 -0
- package/test/generic-order.test.ts +342 -0
- package/test/implicit-batch.test.js +326 -0
- package/test/json-parse-error-handling.test.ts +463 -0
- package/test/retry.test.ts +1016 -0
- package/test/schema.test.js +96 -0
- package/test/streaming.test.ts +316 -0
- package/test/tagged-templates.test.js +240 -0
- package/test/tool-orchestration.test.ts +770 -0
- package/vitest.config.js +39 -0
package/src/types.ts
CHANGED
|
@@ -2,16 +2,15 @@
|
|
|
2
2
|
* Core types for AI functions
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
// Use Promise directly for interface definitions
|
|
6
|
-
// The actual RPC layer handles serialization
|
|
7
|
-
type RpcPromise<T> = Promise<T>
|
|
8
|
-
|
|
9
5
|
/**
|
|
10
6
|
* A function definition that can be called by AI
|
|
7
|
+
*
|
|
8
|
+
* @typeParam TOutput - The return type of the function handler
|
|
9
|
+
* @typeParam TInput - The input type accepted by the function handler
|
|
11
10
|
*/
|
|
12
11
|
export interface AIFunctionDefinition<
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
TOutput = unknown,
|
|
13
|
+
TInput = unknown
|
|
15
14
|
> {
|
|
16
15
|
/** Unique name for the function */
|
|
17
16
|
name: string
|
|
@@ -88,41 +87,41 @@ export interface AIFunctionCall {
|
|
|
88
87
|
}
|
|
89
88
|
|
|
90
89
|
/**
|
|
91
|
-
* AI client interface - all methods return
|
|
90
|
+
* AI client interface - all methods return Promise for pipelining
|
|
92
91
|
*/
|
|
93
92
|
export interface AIClient {
|
|
94
93
|
/** Generate text or structured output */
|
|
95
|
-
generate(options: AIGenerateOptions):
|
|
94
|
+
generate(options: AIGenerateOptions): Promise<AIGenerateResult>
|
|
96
95
|
|
|
97
96
|
/** Execute an action */
|
|
98
|
-
do(action: string, context?: unknown):
|
|
97
|
+
do(action: string, context?: unknown): Promise<unknown>
|
|
99
98
|
|
|
100
99
|
/** Type checking / validation */
|
|
101
|
-
is(value: unknown, type: string | JSONSchema):
|
|
100
|
+
is(value: unknown, type: string | JSONSchema): Promise<boolean>
|
|
102
101
|
|
|
103
102
|
/** Generate code */
|
|
104
|
-
code(prompt: string, language?: string):
|
|
103
|
+
code(prompt: string, language?: string): Promise<string>
|
|
105
104
|
|
|
106
105
|
/** Make a decision */
|
|
107
|
-
decide<T extends string>(options: T[], context?: string):
|
|
106
|
+
decide<T extends string>(options: T[], context?: string): Promise<T>
|
|
108
107
|
|
|
109
108
|
/** Generate a diagram */
|
|
110
|
-
diagram(description: string, format?: 'mermaid' | 'svg' | 'ascii'):
|
|
109
|
+
diagram(description: string, format?: 'mermaid' | 'svg' | 'ascii'): Promise<string>
|
|
111
110
|
|
|
112
111
|
/** Generate an image */
|
|
113
|
-
image(prompt: string, options?: ImageOptions):
|
|
112
|
+
image(prompt: string, options?: ImageOptions): Promise<ImageResult>
|
|
114
113
|
|
|
115
114
|
/** Generate a video */
|
|
116
|
-
video(prompt: string, options?: VideoOptions):
|
|
115
|
+
video(prompt: string, options?: VideoOptions): Promise<VideoResult>
|
|
117
116
|
|
|
118
117
|
/** Write/generate text content */
|
|
119
|
-
write(prompt: string, options?: WriteOptions):
|
|
118
|
+
write(prompt: string, options?: WriteOptions): Promise<string>
|
|
120
119
|
|
|
121
120
|
/** Generate a list of items with names and descriptions */
|
|
122
|
-
list(prompt: string):
|
|
121
|
+
list(prompt: string): Promise<ListResult>
|
|
123
122
|
|
|
124
123
|
/** Generate multiple named lists of items */
|
|
125
|
-
lists(prompt: string):
|
|
124
|
+
lists(prompt: string): Promise<ListsResult>
|
|
126
125
|
}
|
|
127
126
|
|
|
128
127
|
export interface ImageOptions {
|
|
@@ -209,7 +208,7 @@ export type CodeLanguage = 'typescript' | 'javascript' | 'python' | 'go' | 'rust
|
|
|
209
208
|
/**
|
|
210
209
|
* Output types for generative functions
|
|
211
210
|
*/
|
|
212
|
-
export type GenerativeOutputType = 'string' | 'object' | 'image' | 'video'
|
|
211
|
+
export type GenerativeOutputType = 'string' | 'object' | 'image' | 'video'
|
|
213
212
|
|
|
214
213
|
/**
|
|
215
214
|
* Human interaction channels
|
|
@@ -258,16 +257,19 @@ export interface SchemaLimitations {
|
|
|
258
257
|
|
|
259
258
|
/**
|
|
260
259
|
* Base definition shared by all function types
|
|
260
|
+
*
|
|
261
|
+
* @typeParam TOutput - The return type schema
|
|
262
|
+
* @typeParam TInput - The arguments schema
|
|
261
263
|
*/
|
|
262
|
-
export interface BaseFunctionDefinition<
|
|
264
|
+
export interface BaseFunctionDefinition<TOutput = unknown, TInput = unknown> {
|
|
263
265
|
/** Function name (used as the callable identifier) */
|
|
264
266
|
name: string
|
|
265
267
|
/** Human-readable description of what this function does */
|
|
266
268
|
description?: string
|
|
267
269
|
/** Arguments schema - SimpleSchema or Zod schema */
|
|
268
|
-
args:
|
|
270
|
+
args: TInput
|
|
269
271
|
/** Return type schema - SimpleSchema or Zod schema (optional) */
|
|
270
|
-
returnType?:
|
|
272
|
+
returnType?: TOutput
|
|
271
273
|
}
|
|
272
274
|
|
|
273
275
|
/**
|
|
@@ -291,9 +293,12 @@ export interface BaseFunctionDefinition<TArgs = unknown, TReturn = unknown> {
|
|
|
291
293
|
* language: 'typescript',
|
|
292
294
|
* })
|
|
293
295
|
* ```
|
|
296
|
+
*
|
|
297
|
+
* @typeParam TOutput - The return type schema
|
|
298
|
+
* @typeParam TInput - The arguments schema
|
|
294
299
|
*/
|
|
295
|
-
export interface CodeFunctionDefinition<
|
|
296
|
-
extends BaseFunctionDefinition<
|
|
300
|
+
export interface CodeFunctionDefinition<TOutput = unknown, TInput = unknown>
|
|
301
|
+
extends BaseFunctionDefinition<TOutput, TInput> {
|
|
297
302
|
type: 'code'
|
|
298
303
|
/** Target programming language */
|
|
299
304
|
language?: CodeLanguage
|
|
@@ -335,9 +340,12 @@ export interface CodeFunctionResult {
|
|
|
335
340
|
* promptTemplate: 'Summarize the following text:\n\n{{text}}',
|
|
336
341
|
* })
|
|
337
342
|
* ```
|
|
343
|
+
*
|
|
344
|
+
* @typeParam TOutput - The return type schema
|
|
345
|
+
* @typeParam TInput - The arguments schema
|
|
338
346
|
*/
|
|
339
|
-
export interface GenerativeFunctionDefinition<
|
|
340
|
-
extends BaseFunctionDefinition<
|
|
347
|
+
export interface GenerativeFunctionDefinition<TOutput = unknown, TInput = unknown>
|
|
348
|
+
extends BaseFunctionDefinition<TOutput, TInput> {
|
|
341
349
|
type: 'generative'
|
|
342
350
|
/** What type of output this function produces */
|
|
343
351
|
output: GenerativeOutputType
|
|
@@ -363,8 +371,6 @@ export interface GenerativeFunctionResult<T = unknown> {
|
|
|
363
371
|
image?: ImageResult
|
|
364
372
|
/** Generated video (if output is 'video') */
|
|
365
373
|
video?: VideoResult
|
|
366
|
-
/** Generated audio URL (if output is 'audio') */
|
|
367
|
-
audio?: { url: string; duration: number }
|
|
368
374
|
}
|
|
369
375
|
|
|
370
376
|
/**
|
|
@@ -385,9 +391,12 @@ export interface GenerativeFunctionResult<T = unknown> {
|
|
|
385
391
|
* maxIterations: 10,
|
|
386
392
|
* })
|
|
387
393
|
* ```
|
|
394
|
+
*
|
|
395
|
+
* @typeParam TOutput - The return type schema
|
|
396
|
+
* @typeParam TInput - The arguments schema
|
|
388
397
|
*/
|
|
389
|
-
export interface AgenticFunctionDefinition<
|
|
390
|
-
extends BaseFunctionDefinition<
|
|
398
|
+
export interface AgenticFunctionDefinition<TOutput = unknown, TInput = unknown>
|
|
399
|
+
extends BaseFunctionDefinition<TOutput, TInput> {
|
|
391
400
|
type: 'agentic'
|
|
392
401
|
/** Instructions for the agent on how to accomplish the task */
|
|
393
402
|
instructions: string
|
|
@@ -447,9 +456,12 @@ export interface AgenticExecutionState {
|
|
|
447
456
|
* instructions: 'Review the expense request and approve or reject it.',
|
|
448
457
|
* })
|
|
449
458
|
* ```
|
|
459
|
+
*
|
|
460
|
+
* @typeParam TOutput - The return type schema
|
|
461
|
+
* @typeParam TInput - The arguments schema
|
|
450
462
|
*/
|
|
451
|
-
export interface HumanFunctionDefinition<
|
|
452
|
-
extends BaseFunctionDefinition<
|
|
463
|
+
export interface HumanFunctionDefinition<TOutput = unknown, TInput = unknown>
|
|
464
|
+
extends BaseFunctionDefinition<TOutput, TInput> {
|
|
453
465
|
type: 'human'
|
|
454
466
|
/** How to interact with the human */
|
|
455
467
|
channel: HumanChannel
|
|
@@ -488,23 +500,29 @@ export interface HumanFunctionResult<T = unknown> {
|
|
|
488
500
|
|
|
489
501
|
/**
|
|
490
502
|
* Union of all function definition types
|
|
503
|
+
*
|
|
504
|
+
* @typeParam TOutput - The return type schema
|
|
505
|
+
* @typeParam TInput - The arguments schema
|
|
491
506
|
*/
|
|
492
|
-
export type FunctionDefinition<
|
|
493
|
-
| CodeFunctionDefinition<
|
|
494
|
-
| GenerativeFunctionDefinition<
|
|
495
|
-
| AgenticFunctionDefinition<
|
|
496
|
-
| HumanFunctionDefinition<
|
|
507
|
+
export type FunctionDefinition<TOutput = unknown, TInput = unknown> =
|
|
508
|
+
| CodeFunctionDefinition<TOutput, TInput>
|
|
509
|
+
| GenerativeFunctionDefinition<TOutput, TInput>
|
|
510
|
+
| AgenticFunctionDefinition<TOutput, TInput>
|
|
511
|
+
| HumanFunctionDefinition<TOutput, TInput>
|
|
497
512
|
|
|
498
513
|
/**
|
|
499
514
|
* Result of defineFunction - a callable with metadata
|
|
515
|
+
*
|
|
516
|
+
* @typeParam TOutput - The return type of the function
|
|
517
|
+
* @typeParam TInput - The arguments type accepted by the function
|
|
500
518
|
*/
|
|
501
|
-
export interface DefinedFunction<
|
|
519
|
+
export interface DefinedFunction<TOutput = unknown, TInput = unknown> {
|
|
502
520
|
/** The original definition */
|
|
503
|
-
definition: FunctionDefinition<
|
|
521
|
+
definition: FunctionDefinition<TOutput, TInput>
|
|
504
522
|
/** Call the function */
|
|
505
|
-
call: (args:
|
|
523
|
+
call: (args: TInput) => Promise<TOutput>
|
|
506
524
|
/** Get the function as a tool definition for AI */
|
|
507
|
-
asTool: () => AIFunctionDefinition<
|
|
525
|
+
asTool: () => AIFunctionDefinition<TOutput, TInput>
|
|
508
526
|
}
|
|
509
527
|
|
|
510
528
|
/**
|
|
@@ -0,0 +1,157 @@
|
|
|
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
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
7
|
+
import { ai, AI, functions, withTemplate } from '../src/index.js';
|
|
8
|
+
// Skip tests if no gateway configured
|
|
9
|
+
const hasGateway = !!process.env.AI_GATEWAY_URL || !!process.env.ANTHROPIC_API_KEY;
|
|
10
|
+
describe('ai proxy', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
functions.clear();
|
|
13
|
+
});
|
|
14
|
+
it('exposes functions registry', () => {
|
|
15
|
+
expect(ai.functions).toBeDefined();
|
|
16
|
+
expect(typeof ai.functions.list).toBe('function');
|
|
17
|
+
expect(typeof ai.functions.get).toBe('function');
|
|
18
|
+
expect(typeof ai.functions.set).toBe('function');
|
|
19
|
+
expect(typeof ai.functions.has).toBe('function');
|
|
20
|
+
expect(typeof ai.functions.clear).toBe('function');
|
|
21
|
+
expect(typeof ai.functions.delete).toBe('function');
|
|
22
|
+
});
|
|
23
|
+
it('exposes define helpers', () => {
|
|
24
|
+
expect(ai.define).toBeDefined();
|
|
25
|
+
expect(typeof ai.define).toBe('function');
|
|
26
|
+
expect(typeof ai.define.generative).toBe('function');
|
|
27
|
+
expect(typeof ai.define.agentic).toBe('function');
|
|
28
|
+
expect(typeof ai.define.human).toBe('function');
|
|
29
|
+
expect(typeof ai.define.code).toBe('function');
|
|
30
|
+
});
|
|
31
|
+
it('exposes defineFunction', () => {
|
|
32
|
+
expect(typeof ai.defineFunction).toBe('function');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe.skipIf(!hasGateway)('ai proxy auto-define', () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
functions.clear();
|
|
38
|
+
});
|
|
39
|
+
it('auto-defines a function on first call', async () => {
|
|
40
|
+
expect(functions.has('greetPerson')).toBe(false);
|
|
41
|
+
const result = await ai.greetPerson({
|
|
42
|
+
name: 'Alice',
|
|
43
|
+
style: 'friendly',
|
|
44
|
+
});
|
|
45
|
+
expect(result).toBeDefined();
|
|
46
|
+
expect(functions.has('greetPerson')).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
it('uses cached definition on second call', async () => {
|
|
49
|
+
// First call - defines the function
|
|
50
|
+
await ai.capitalizeText({
|
|
51
|
+
text: 'hello',
|
|
52
|
+
});
|
|
53
|
+
const fn1 = functions.get('capitalizeText');
|
|
54
|
+
expect(fn1).toBeDefined();
|
|
55
|
+
// Second call - uses cached definition
|
|
56
|
+
await ai.capitalizeText({
|
|
57
|
+
text: 'world',
|
|
58
|
+
});
|
|
59
|
+
const fn2 = functions.get('capitalizeText');
|
|
60
|
+
expect(fn1).toBe(fn2); // Same cached function
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe.skipIf(!hasGateway)('AI() schema functions', () => {
|
|
64
|
+
it('creates schema-based functions', async () => {
|
|
65
|
+
const client = AI({
|
|
66
|
+
sentiment: {
|
|
67
|
+
sentiment: 'positive | negative | neutral',
|
|
68
|
+
score: 'Confidence score 0-1 (number)',
|
|
69
|
+
explanation: 'Brief explanation',
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
expect(client.sentiment).toBeDefined();
|
|
73
|
+
expect(typeof client.sentiment).toBe('function');
|
|
74
|
+
});
|
|
75
|
+
it('generates structured output from schema', async () => {
|
|
76
|
+
const client = AI({
|
|
77
|
+
person: {
|
|
78
|
+
name: 'Full name',
|
|
79
|
+
age: 'Age (number)',
|
|
80
|
+
occupation: 'Job title',
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
const result = await client.person('A software engineer named Alice who is 30');
|
|
84
|
+
expect(result).toBeDefined();
|
|
85
|
+
expect(typeof result.name).toBe('string');
|
|
86
|
+
expect(typeof result.age).toBe('number');
|
|
87
|
+
expect(typeof result.occupation).toBe('string');
|
|
88
|
+
});
|
|
89
|
+
it('generates nested objects', async () => {
|
|
90
|
+
const client = AI({
|
|
91
|
+
profile: {
|
|
92
|
+
user: {
|
|
93
|
+
name: 'Name',
|
|
94
|
+
email: 'Email address',
|
|
95
|
+
},
|
|
96
|
+
preferences: {
|
|
97
|
+
theme: 'light | dark',
|
|
98
|
+
notifications: 'Enabled? (boolean)',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
const result = await client.profile('User Alice who prefers dark mode and has notifications on');
|
|
103
|
+
expect(result).toBeDefined();
|
|
104
|
+
expect(result.user).toBeDefined();
|
|
105
|
+
expect(result.preferences).toBeDefined();
|
|
106
|
+
expect(['light', 'dark']).toContain(result.preferences.theme);
|
|
107
|
+
expect(typeof result.preferences.notifications).toBe('boolean');
|
|
108
|
+
});
|
|
109
|
+
it('generates arrays', async () => {
|
|
110
|
+
const client = AI({
|
|
111
|
+
todoList: {
|
|
112
|
+
title: 'List title',
|
|
113
|
+
items: ['Todo items'],
|
|
114
|
+
priority: 'high | medium | low',
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
const result = await client.todoList('A high priority shopping list with 3 items');
|
|
118
|
+
expect(result).toBeDefined();
|
|
119
|
+
expect(typeof result.title).toBe('string');
|
|
120
|
+
expect(Array.isArray(result.items)).toBe(true);
|
|
121
|
+
expect(result.items.length).toBeGreaterThan(0);
|
|
122
|
+
expect(['high', 'medium', 'low']).toContain(result.priority);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
describe('withTemplate helper', () => {
|
|
126
|
+
it('handles regular function calls', () => {
|
|
127
|
+
const fn = withTemplate((prompt) => prompt.toUpperCase());
|
|
128
|
+
const result = fn('hello world');
|
|
129
|
+
expect(result).toBe('HELLO WORLD');
|
|
130
|
+
});
|
|
131
|
+
it('handles tagged template literals', () => {
|
|
132
|
+
const fn = withTemplate((prompt) => prompt.toUpperCase());
|
|
133
|
+
const result = fn `hello world`;
|
|
134
|
+
expect(result).toBe('HELLO WORLD');
|
|
135
|
+
});
|
|
136
|
+
it('handles tagged template literals with interpolation', () => {
|
|
137
|
+
const fn = withTemplate((prompt) => prompt.toUpperCase());
|
|
138
|
+
const name = 'Alice';
|
|
139
|
+
const result = fn `hello ${name}!`;
|
|
140
|
+
expect(result).toBe('HELLO ALICE!');
|
|
141
|
+
});
|
|
142
|
+
it('handles multiple interpolations', () => {
|
|
143
|
+
const fn = withTemplate((prompt) => prompt);
|
|
144
|
+
const a = 'one';
|
|
145
|
+
const b = 'two';
|
|
146
|
+
const c = 'three';
|
|
147
|
+
const result = fn `${a}, ${b}, ${c}`;
|
|
148
|
+
expect(result).toBe('one, two, three');
|
|
149
|
+
});
|
|
150
|
+
it('works with async functions', async () => {
|
|
151
|
+
const fn = withTemplate(async (prompt) => {
|
|
152
|
+
return `Result: ${prompt}`;
|
|
153
|
+
});
|
|
154
|
+
const result = await fn `async test`;
|
|
155
|
+
expect(result).toBe('Result: async test');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,261 @@
|
|
|
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
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Mock async generators
|
|
11
|
+
// ============================================================================
|
|
12
|
+
const mockStreamItems = vi.fn();
|
|
13
|
+
/**
|
|
14
|
+
* Create an async iterable from an array (simulates streaming)
|
|
15
|
+
*/
|
|
16
|
+
async function* createAsyncIterable(items, delayMs = 10) {
|
|
17
|
+
for (const item of items) {
|
|
18
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
19
|
+
yield item;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Mock list function that returns both a promise and an async iterable
|
|
24
|
+
*/
|
|
25
|
+
function createMockStreamingList() {
|
|
26
|
+
return function list(promptOrStrings, ...args) {
|
|
27
|
+
let prompt;
|
|
28
|
+
if (Array.isArray(promptOrStrings) && 'raw' in promptOrStrings) {
|
|
29
|
+
prompt = promptOrStrings.reduce((acc, str, i) => {
|
|
30
|
+
return acc + str + (args[i] ?? '');
|
|
31
|
+
}, '');
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
prompt = promptOrStrings;
|
|
35
|
+
}
|
|
36
|
+
const items = mockStreamItems(prompt);
|
|
37
|
+
// Return an object that is both a Promise and AsyncIterable
|
|
38
|
+
const asyncIterable = createAsyncIterable(items);
|
|
39
|
+
const result = {
|
|
40
|
+
// Promise interface - resolve to full array
|
|
41
|
+
then: (resolve, reject) => {
|
|
42
|
+
return Promise.resolve(items).then(resolve, reject);
|
|
43
|
+
},
|
|
44
|
+
// AsyncIterable interface - stream items
|
|
45
|
+
[Symbol.asyncIterator]: () => asyncIterable[Symbol.asyncIterator](),
|
|
46
|
+
};
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Mock extract function with streaming support
|
|
52
|
+
*/
|
|
53
|
+
function createMockStreamingExtract() {
|
|
54
|
+
return function extract(promptOrStrings, ...args) {
|
|
55
|
+
let prompt;
|
|
56
|
+
if (Array.isArray(promptOrStrings) && 'raw' in promptOrStrings) {
|
|
57
|
+
prompt = promptOrStrings.reduce((acc, str, i) => {
|
|
58
|
+
return acc + str + (args[i] ?? '');
|
|
59
|
+
}, '');
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
prompt = promptOrStrings;
|
|
63
|
+
}
|
|
64
|
+
const items = mockStreamItems(prompt);
|
|
65
|
+
const asyncIterable = createAsyncIterable(items);
|
|
66
|
+
const result = {
|
|
67
|
+
then: (resolve, reject) => {
|
|
68
|
+
return Promise.resolve(items).then(resolve, reject);
|
|
69
|
+
},
|
|
70
|
+
[Symbol.asyncIterator]: () => asyncIterable[Symbol.asyncIterator](),
|
|
71
|
+
};
|
|
72
|
+
return result;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// list() async iterator tests
|
|
77
|
+
// ============================================================================
|
|
78
|
+
describe('list() async iteration', () => {
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
mockStreamItems.mockReset();
|
|
81
|
+
});
|
|
82
|
+
it('can be awaited to get full array', async () => {
|
|
83
|
+
mockStreamItems.mockReturnValue(['Idea 1', 'Idea 2', 'Idea 3']);
|
|
84
|
+
const list = createMockStreamingList();
|
|
85
|
+
const result = await list `startup ideas`;
|
|
86
|
+
expect(Array.isArray(result)).toBe(true);
|
|
87
|
+
expect(result).toHaveLength(3);
|
|
88
|
+
});
|
|
89
|
+
it('can be iterated with for await', async () => {
|
|
90
|
+
mockStreamItems.mockReturnValue(['Idea 1', 'Idea 2', 'Idea 3']);
|
|
91
|
+
const list = createMockStreamingList();
|
|
92
|
+
const collected = [];
|
|
93
|
+
for await (const item of list `startup ideas`) {
|
|
94
|
+
collected.push(item);
|
|
95
|
+
}
|
|
96
|
+
expect(collected).toHaveLength(3);
|
|
97
|
+
expect(collected).toEqual(['Idea 1', 'Idea 2', 'Idea 3']);
|
|
98
|
+
});
|
|
99
|
+
it('allows early termination with break', async () => {
|
|
100
|
+
mockStreamItems.mockReturnValue(['Idea 1', 'Billion Dollar Idea', 'Idea 3', 'Idea 4', 'Idea 5']);
|
|
101
|
+
const list = createMockStreamingList();
|
|
102
|
+
const collected = [];
|
|
103
|
+
for await (const idea of list `startup ideas`) {
|
|
104
|
+
collected.push(idea);
|
|
105
|
+
if (idea.includes('Billion')) {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Should have stopped after finding the billion dollar idea
|
|
110
|
+
expect(collected).toHaveLength(2);
|
|
111
|
+
expect(collected[1]).toBe('Billion Dollar Idea');
|
|
112
|
+
});
|
|
113
|
+
it('supports nested iteration pattern from README', async () => {
|
|
114
|
+
const marketList = createMockStreamingList();
|
|
115
|
+
const icpList = createMockStreamingList();
|
|
116
|
+
// Simulate the nested pattern
|
|
117
|
+
mockStreamItems
|
|
118
|
+
.mockReturnValueOnce(['Market A', 'Market B'])
|
|
119
|
+
.mockReturnValueOnce(['ICP 1', 'ICP 2'])
|
|
120
|
+
.mockReturnValueOnce(['ICP 3', 'ICP 4']);
|
|
121
|
+
const results = [];
|
|
122
|
+
for await (const market of marketList `market segments`) {
|
|
123
|
+
for await (const icp of icpList `customer profiles for ${market}`) {
|
|
124
|
+
results.push({ market, icp });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
expect(results).toHaveLength(4);
|
|
128
|
+
expect(results[0]).toEqual({ market: 'Market A', icp: 'ICP 1' });
|
|
129
|
+
expect(results[3]).toEqual({ market: 'Market B', icp: 'ICP 4' });
|
|
130
|
+
});
|
|
131
|
+
it('processes items as they stream in', async () => {
|
|
132
|
+
mockStreamItems.mockReturnValue(['Item 1', 'Item 2', 'Item 3']);
|
|
133
|
+
const list = createMockStreamingList();
|
|
134
|
+
const processedAt = [];
|
|
135
|
+
const startTime = Date.now();
|
|
136
|
+
for await (const _item of list `items`) {
|
|
137
|
+
processedAt.push(Date.now() - startTime);
|
|
138
|
+
}
|
|
139
|
+
// Items should be processed incrementally, not all at once
|
|
140
|
+
expect(processedAt[1]).toBeGreaterThan(processedAt[0]);
|
|
141
|
+
expect(processedAt[2]).toBeGreaterThan(processedAt[1]);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
// ============================================================================
|
|
145
|
+
// extract() async iterator tests
|
|
146
|
+
// ============================================================================
|
|
147
|
+
describe('extract() async iteration', () => {
|
|
148
|
+
beforeEach(() => {
|
|
149
|
+
mockStreamItems.mockReset();
|
|
150
|
+
});
|
|
151
|
+
it('can be awaited to get full array', async () => {
|
|
152
|
+
mockStreamItems.mockReturnValue(['John Smith', 'Jane Doe', 'Bob Wilson']);
|
|
153
|
+
const extract = createMockStreamingExtract();
|
|
154
|
+
const result = await extract `names from article`;
|
|
155
|
+
expect(Array.isArray(result)).toBe(true);
|
|
156
|
+
expect(result).toHaveLength(3);
|
|
157
|
+
});
|
|
158
|
+
it('can be iterated with for await', async () => {
|
|
159
|
+
mockStreamItems.mockReturnValue(['email1@example.com', 'email2@example.com']);
|
|
160
|
+
const extract = createMockStreamingExtract();
|
|
161
|
+
const collected = [];
|
|
162
|
+
for await (const email of extract `email addresses from document`) {
|
|
163
|
+
collected.push(email);
|
|
164
|
+
}
|
|
165
|
+
expect(collected).toHaveLength(2);
|
|
166
|
+
});
|
|
167
|
+
it('supports the research + extract pattern from README', async () => {
|
|
168
|
+
mockStreamItems.mockReturnValue(['Competitor A', 'Competitor B', 'Competitor C']);
|
|
169
|
+
const extract = createMockStreamingExtract();
|
|
170
|
+
const competitors = [];
|
|
171
|
+
const marketResearch = 'Report mentioning Competitor A, Competitor B, and Competitor C...';
|
|
172
|
+
for await (const competitor of extract `company names from ${marketResearch}`) {
|
|
173
|
+
competitors.push(competitor);
|
|
174
|
+
// In real code, you would do: await research`${competitor} vs ${ourProduct}`
|
|
175
|
+
}
|
|
176
|
+
expect(competitors).toHaveLength(3);
|
|
177
|
+
});
|
|
178
|
+
it('allows processing each extraction as it completes', async () => {
|
|
179
|
+
mockStreamItems.mockReturnValue(['email1@test.com', 'email2@test.com']);
|
|
180
|
+
const extract = createMockStreamingExtract();
|
|
181
|
+
const notifications = [];
|
|
182
|
+
for await (const email of extract `emails from document`) {
|
|
183
|
+
// Simulate sending notification
|
|
184
|
+
notifications.push(`Notified: ${email}`);
|
|
185
|
+
}
|
|
186
|
+
expect(notifications).toHaveLength(2);
|
|
187
|
+
expect(notifications[0]).toBe('Notified: email1@test.com');
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
// ============================================================================
|
|
191
|
+
// Combined patterns
|
|
192
|
+
// ============================================================================
|
|
193
|
+
describe('combined async iteration patterns', () => {
|
|
194
|
+
beforeEach(() => {
|
|
195
|
+
mockStreamItems.mockReset();
|
|
196
|
+
});
|
|
197
|
+
it('supports the full market research pattern from README', async () => {
|
|
198
|
+
const list = createMockStreamingList();
|
|
199
|
+
const extract = createMockStreamingExtract();
|
|
200
|
+
// Mock different results for each call
|
|
201
|
+
mockStreamItems
|
|
202
|
+
.mockReturnValueOnce(['Market A']) // markets
|
|
203
|
+
.mockReturnValueOnce(['ICP 1']) // ICPs for Market A
|
|
204
|
+
.mockReturnValueOnce(['Blog 1', 'Blog 2']); // blog titles
|
|
205
|
+
const results = [];
|
|
206
|
+
// Simplified version of README example
|
|
207
|
+
for await (const market of list `market segments for idea`) {
|
|
208
|
+
for await (const icp of list `customer profiles for ${market}`) {
|
|
209
|
+
for await (const title of list `blog titles for ${icp}`) {
|
|
210
|
+
results.push(`${market} > ${icp} > ${title}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
expect(results).toHaveLength(2);
|
|
215
|
+
expect(results[0]).toBe('Market A > ICP 1 > Blog 1');
|
|
216
|
+
expect(results[1]).toBe('Market A > ICP 1 > Blog 2');
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
// ============================================================================
|
|
220
|
+
// Type safety
|
|
221
|
+
// ============================================================================
|
|
222
|
+
describe('async iterator type safety', () => {
|
|
223
|
+
it('list returns string items by default', async () => {
|
|
224
|
+
mockStreamItems.mockReturnValue(['a', 'b', 'c']);
|
|
225
|
+
const list = createMockStreamingList();
|
|
226
|
+
for await (const item of list `items`) {
|
|
227
|
+
expect(typeof item).toBe('string');
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
it('extract can return typed objects with schema', async () => {
|
|
231
|
+
// This tests the concept - actual implementation would use schema
|
|
232
|
+
const items = [
|
|
233
|
+
{ name: 'Acme', role: 'competitor' },
|
|
234
|
+
{ name: 'Beta', role: 'partner' },
|
|
235
|
+
];
|
|
236
|
+
mockStreamItems.mockReturnValue(items);
|
|
237
|
+
const extract = createMockStreamingExtract();
|
|
238
|
+
const collected = [];
|
|
239
|
+
for await (const company of extract `companies from text`) {
|
|
240
|
+
collected.push(company);
|
|
241
|
+
}
|
|
242
|
+
expect(collected[0]).toHaveProperty('name');
|
|
243
|
+
expect(collected[0]).toHaveProperty('role');
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
// ============================================================================
|
|
247
|
+
// Error handling
|
|
248
|
+
// ============================================================================
|
|
249
|
+
describe('async iterator error handling', () => {
|
|
250
|
+
it('propagates errors during iteration', async () => {
|
|
251
|
+
mockStreamItems.mockImplementation(() => {
|
|
252
|
+
throw new Error('Generation failed');
|
|
253
|
+
});
|
|
254
|
+
const list = createMockStreamingList();
|
|
255
|
+
await expect(async () => {
|
|
256
|
+
for await (const _item of list `items`) {
|
|
257
|
+
// Should not reach here
|
|
258
|
+
}
|
|
259
|
+
}).rejects.toThrow('Generation failed');
|
|
260
|
+
});
|
|
261
|
+
});
|