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.
@@ -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
+ }