llm-fns 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/package.json +27 -0
- package/readme.md +299 -0
- package/scripts/release.sh +32 -0
- package/src/createLlmClient.spec.ts +42 -0
- package/src/createLlmClient.ts +389 -0
- package/src/createLlmRetryClient.ts +244 -0
- package/src/createZodLlmClient.spec.ts +76 -0
- package/src/createZodLlmClient.ts +378 -0
- package/src/index.ts +5 -0
- package/src/llmFactory.ts +26 -0
- package/src/retryUtils.ts +91 -0
- package/tests/basic.test.ts +47 -0
- package/tests/env.ts +16 -0
- package/tests/setup.ts +24 -0
- package/tests/zod.test.ts +178 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { createTestLlm } from './setup.js';
|
|
4
|
+
import { createZodLlmClient } from '../src/createZodLlmClient.js';
|
|
5
|
+
import { LlmRetryExhaustedError } from '../src/createLlmRetryClient.js';
|
|
6
|
+
|
|
7
|
+
// Helper to create a mock prompt function
|
|
8
|
+
function createMockPrompt(responses: string[]) {
|
|
9
|
+
let callCount = 0;
|
|
10
|
+
return vi.fn(async (...args: any[]) => {
|
|
11
|
+
const content = responses[callCount] || responses[responses.length - 1];
|
|
12
|
+
callCount++;
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
id: 'mock-id',
|
|
16
|
+
object: 'chat.completion',
|
|
17
|
+
created: Date.now(),
|
|
18
|
+
model: 'mock-model',
|
|
19
|
+
choices: [{
|
|
20
|
+
message: { role: 'assistant', content },
|
|
21
|
+
finish_reason: 'stop',
|
|
22
|
+
index: 0
|
|
23
|
+
}],
|
|
24
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
25
|
+
} as any;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('Zod Structured Output Integration', () => {
|
|
30
|
+
it('should extract structured data matching a schema', async () => {
|
|
31
|
+
const { llm } = await createTestLlm();
|
|
32
|
+
|
|
33
|
+
const InventorySchema = z.object({
|
|
34
|
+
items: z.array(z.object({
|
|
35
|
+
name: z.string(),
|
|
36
|
+
quantity: z.number(),
|
|
37
|
+
color: z.string().optional(),
|
|
38
|
+
}))
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const text = "I have 3 red apples and 2 yellow bananas in my basket.";
|
|
42
|
+
|
|
43
|
+
const result = await llm.promptZod(
|
|
44
|
+
"Extract the inventory items from the user text.",
|
|
45
|
+
[{ type: 'text', text }],
|
|
46
|
+
InventorySchema
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(result).toBeDefined();
|
|
50
|
+
expect(result.items).toHaveLength(2);
|
|
51
|
+
|
|
52
|
+
const apple = result.items.find(i => i.name.toLowerCase().includes('apple'));
|
|
53
|
+
expect(apple).toBeDefined();
|
|
54
|
+
expect(apple?.quantity).toBe(3);
|
|
55
|
+
expect(apple?.color).toBe('red');
|
|
56
|
+
|
|
57
|
+
const banana = result.items.find(i => i.name.toLowerCase().includes('banana'));
|
|
58
|
+
expect(banana).toBeDefined();
|
|
59
|
+
expect(banana?.quantity).toBe(2);
|
|
60
|
+
expect(banana?.color).toBe('yellow');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('Retry Mechanisms (Mocked)', () => {
|
|
64
|
+
const Schema = z.object({
|
|
65
|
+
age: z.number()
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should fix invalid JSON syntax using the internal fixer', async () => {
|
|
69
|
+
const mockPrompt = createMockPrompt([
|
|
70
|
+
'{"age": 20', // Missing closing brace
|
|
71
|
+
'{"age": 20}' // Fixed
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
const client = createZodLlmClient({
|
|
75
|
+
prompt: mockPrompt,
|
|
76
|
+
isPromptCached: async () => false,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const result = await client.promptZod("test", "test", Schema);
|
|
80
|
+
|
|
81
|
+
expect(result.age).toBe(20);
|
|
82
|
+
expect(mockPrompt).toHaveBeenCalledTimes(2);
|
|
83
|
+
|
|
84
|
+
// The second call should be the fixer prompt
|
|
85
|
+
// args[0] is options object
|
|
86
|
+
const secondCallArgs = mockPrompt.mock.calls[1][0] as any;
|
|
87
|
+
expect(secondCallArgs.messages[1].content).toContain('BROKEN RESPONSE');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should fix schema validation errors using the internal fixer', async () => {
|
|
91
|
+
const mockPrompt = createMockPrompt([
|
|
92
|
+
'{"age": "twenty"}', // String instead of number
|
|
93
|
+
'{"age": 20}' // Fixed
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
const client = createZodLlmClient({
|
|
97
|
+
prompt: mockPrompt,
|
|
98
|
+
isPromptCached: async () => false,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const result = await client.promptZod("test", "test", Schema);
|
|
102
|
+
|
|
103
|
+
expect(result.age).toBe(20);
|
|
104
|
+
expect(mockPrompt).toHaveBeenCalledTimes(2);
|
|
105
|
+
|
|
106
|
+
const secondCallArgs = mockPrompt.mock.calls[1][0] as any;
|
|
107
|
+
expect(secondCallArgs.messages[1].content).toContain('Schema Validation Error');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should use the main retry loop when internal fixer is disabled', async () => {
|
|
111
|
+
const mockPrompt = createMockPrompt([
|
|
112
|
+
'{"age": "wrong"}', // Initial wrong response
|
|
113
|
+
'{"age": 20}' // Corrected in next turn
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
const client = createZodLlmClient({
|
|
117
|
+
prompt: mockPrompt,
|
|
118
|
+
isPromptCached: async () => false,
|
|
119
|
+
disableJsonFixer: true // Force main loop
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const result = await client.promptZod("test", "test", Schema);
|
|
123
|
+
|
|
124
|
+
expect(result.age).toBe(20);
|
|
125
|
+
expect(mockPrompt).toHaveBeenCalledTimes(2);
|
|
126
|
+
|
|
127
|
+
// In the main loop, the history is preserved and error is added as user message
|
|
128
|
+
const secondCallArgs = mockPrompt.mock.calls[1][0] as any;
|
|
129
|
+
const messages = secondCallArgs.messages;
|
|
130
|
+
// 0: System, 1: User, 2: Assistant (wrong), 3: User (Error feedback)
|
|
131
|
+
expect(messages.length).toBe(4);
|
|
132
|
+
expect(messages[3].role).toBe('user');
|
|
133
|
+
expect(messages[3].content).toContain('SCHEMA_VALIDATION_ERROR');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should switch to fallback prompt on retry if provided', async () => {
|
|
137
|
+
const mockMainPrompt = createMockPrompt([
|
|
138
|
+
'{"age": "wrong"}' // Fails schema
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
const mockFallbackPrompt = createMockPrompt([
|
|
142
|
+
'{"age": 20}' // Succeeds
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
const client = createZodLlmClient({
|
|
146
|
+
prompt: mockMainPrompt,
|
|
147
|
+
fallbackPrompt: mockFallbackPrompt,
|
|
148
|
+
isPromptCached: async () => false,
|
|
149
|
+
disableJsonFixer: true // Force error to retry loop immediately
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const result = await client.promptZod("test", "test", Schema);
|
|
153
|
+
|
|
154
|
+
expect(result.age).toBe(20);
|
|
155
|
+
expect(mockMainPrompt).toHaveBeenCalledTimes(1);
|
|
156
|
+
expect(mockFallbackPrompt).toHaveBeenCalledTimes(1);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should throw LlmRetryExhaustedError when retries are exhausted', async () => {
|
|
160
|
+
const mockPrompt = createMockPrompt([
|
|
161
|
+
'{"age": "wrong"}',
|
|
162
|
+
'{"age": "still wrong"}',
|
|
163
|
+
'{"age": "forever wrong"}'
|
|
164
|
+
]);
|
|
165
|
+
|
|
166
|
+
const client = createZodLlmClient({
|
|
167
|
+
prompt: mockPrompt,
|
|
168
|
+
isPromptCached: async () => false,
|
|
169
|
+
disableJsonFixer: true
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await expect(client.promptZod("test", "test", Schema, { maxRetries: 1 }))
|
|
173
|
+
.rejects.toThrow(LlmRetryExhaustedError);
|
|
174
|
+
|
|
175
|
+
expect(mockPrompt).toHaveBeenCalledTimes(2); // Initial + 1 retry
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"allowJs": true
|
|
12
|
+
},
|
|
13
|
+
"ts-node": {
|
|
14
|
+
"esm": true
|
|
15
|
+
},
|
|
16
|
+
"include": [
|
|
17
|
+
"src/**/*"
|
|
18
|
+
],
|
|
19
|
+
"exclude": [
|
|
20
|
+
"node_modules"
|
|
21
|
+
]
|
|
22
|
+
}
|