llm-fns 1.0.6 → 1.0.8
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/dist/createJsonSchemaLlmClient.d.ts +29 -0
- package/dist/createJsonSchemaLlmClient.js +190 -0
- package/dist/createZodLlmClient.d.ts +3 -21
- package/dist/createZodLlmClient.js +14 -180
- package/dist/createZodLlmClient.spec.js +13 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/llmFactory.d.ts +2 -0
- package/dist/llmFactory.js +6 -1
- package/package.json +1 -1
- package/readme.md +34 -32
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { PromptFunction, LlmPromptOptions, IsPromptCachedFunction } from "./createLlmClient.js";
|
|
3
|
+
export type JsonSchemaLlmClientOptions = Omit<LlmPromptOptions, 'messages' | 'response_format'> & {
|
|
4
|
+
maxRetries?: number;
|
|
5
|
+
/**
|
|
6
|
+
* If true, passes `response_format: { type: 'json_object' }` to the model.
|
|
7
|
+
* If false, only includes the schema in the system prompt.
|
|
8
|
+
* Defaults to true.
|
|
9
|
+
*/
|
|
10
|
+
useResponseFormat?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* A hook to process the parsed JSON data before it is validated.
|
|
13
|
+
* This can be used to merge partial results or perform other transformations.
|
|
14
|
+
* @param data The parsed JSON data from the LLM response.
|
|
15
|
+
* @returns The processed data to be validated.
|
|
16
|
+
*/
|
|
17
|
+
beforeValidation?: (data: any) => any;
|
|
18
|
+
};
|
|
19
|
+
export interface CreateJsonSchemaLlmClientParams {
|
|
20
|
+
prompt: PromptFunction;
|
|
21
|
+
isPromptCached: IsPromptCachedFunction;
|
|
22
|
+
fallbackPrompt?: PromptFunction;
|
|
23
|
+
disableJsonFixer?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare function createJsonSchemaLlmClient(params: CreateJsonSchemaLlmClientParams): {
|
|
26
|
+
promptJson: <T>(messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[], schema: Record<string, any>, validator: (data: any) => T, options?: JsonSchemaLlmClientOptions) => Promise<T>;
|
|
27
|
+
isPromptJsonCached: (messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[], schema: Record<string, any>, options?: JsonSchemaLlmClientOptions) => Promise<boolean>;
|
|
28
|
+
};
|
|
29
|
+
export type JsonSchemaClient = ReturnType<typeof createJsonSchemaLlmClient>;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createJsonSchemaLlmClient = createJsonSchemaLlmClient;
|
|
4
|
+
const createLlmRetryClient_js_1 = require("./createLlmRetryClient.js");
|
|
5
|
+
function createJsonSchemaLlmClient(params) {
|
|
6
|
+
const { prompt, isPromptCached, fallbackPrompt, disableJsonFixer = false } = params;
|
|
7
|
+
const llmRetryClient = (0, createLlmRetryClient_js_1.createLlmRetryClient)({ prompt, fallbackPrompt });
|
|
8
|
+
async function _tryToFixJson(brokenResponse, schemaJsonString, errorDetails, options) {
|
|
9
|
+
const fixupPrompt = `
|
|
10
|
+
An attempt to generate a JSON object resulted in the following output, which is either not valid JSON or does not conform to the required schema.
|
|
11
|
+
|
|
12
|
+
Your task is to act as a JSON fixer. Analyze the provided "BROKEN RESPONSE" and correct it to match the "REQUIRED JSON SCHEMA".
|
|
13
|
+
|
|
14
|
+
- If the broken response contains all the necessary information to create a valid JSON object according to the schema, please provide the corrected, valid JSON object.
|
|
15
|
+
- If the broken response is missing essential information, or is too garbled to be fixed, please respond with the exact string: "CANNOT_FIX".
|
|
16
|
+
- Your response must be ONLY the corrected JSON object or the string "CANNOT_FIX". Do not include any other text, explanations, or markdown formatting.
|
|
17
|
+
|
|
18
|
+
REQUIRED JSON SCHEMA:
|
|
19
|
+
${schemaJsonString}
|
|
20
|
+
|
|
21
|
+
ERROR DETAILS:
|
|
22
|
+
${errorDetails}
|
|
23
|
+
|
|
24
|
+
BROKEN RESPONSE:
|
|
25
|
+
${brokenResponse}
|
|
26
|
+
`;
|
|
27
|
+
const messages = [
|
|
28
|
+
{ role: 'system', content: 'You are an expert at fixing malformed JSON data to match a specific schema.' },
|
|
29
|
+
{ role: 'user', content: fixupPrompt }
|
|
30
|
+
];
|
|
31
|
+
const useResponseFormat = options?.useResponseFormat ?? true;
|
|
32
|
+
const response_format = useResponseFormat
|
|
33
|
+
? { type: 'json_object' }
|
|
34
|
+
: undefined;
|
|
35
|
+
const { maxRetries, useResponseFormat: _useResponseFormat, ...restOptions } = options || {};
|
|
36
|
+
const completion = await prompt({
|
|
37
|
+
messages,
|
|
38
|
+
response_format,
|
|
39
|
+
...restOptions
|
|
40
|
+
});
|
|
41
|
+
const fixedResponse = completion.choices[0]?.message?.content;
|
|
42
|
+
if (fixedResponse && fixedResponse.trim() === 'CANNOT_FIX') {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return fixedResponse || null;
|
|
46
|
+
}
|
|
47
|
+
async function _parseOrFixJson(llmResponseString, schemaJsonString, options) {
|
|
48
|
+
let jsonDataToParse = llmResponseString.trim();
|
|
49
|
+
// Robust handling for responses wrapped in markdown code blocks
|
|
50
|
+
const codeBlockRegex = /```(?:json)?\s*([\s\S]*?)\s*```/;
|
|
51
|
+
const match = codeBlockRegex.exec(jsonDataToParse);
|
|
52
|
+
if (match && match[1]) {
|
|
53
|
+
jsonDataToParse = match[1].trim();
|
|
54
|
+
}
|
|
55
|
+
if (jsonDataToParse === "") {
|
|
56
|
+
throw new Error("LLM returned an empty string.");
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(jsonDataToParse);
|
|
60
|
+
}
|
|
61
|
+
catch (parseError) {
|
|
62
|
+
if (disableJsonFixer) {
|
|
63
|
+
throw parseError; // re-throw original error
|
|
64
|
+
}
|
|
65
|
+
// Attempt a one-time fix before failing.
|
|
66
|
+
const errorDetails = `JSON Parse Error: ${parseError.message}`;
|
|
67
|
+
const fixedResponse = await _tryToFixJson(jsonDataToParse, schemaJsonString, errorDetails, options);
|
|
68
|
+
if (fixedResponse) {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(fixedResponse);
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
// Fix-up failed, throw original error.
|
|
74
|
+
throw parseError;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
throw parseError; // if no fixed response
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function _validateOrFix(jsonData, validator, schemaJsonString, options) {
|
|
81
|
+
try {
|
|
82
|
+
if (options?.beforeValidation) {
|
|
83
|
+
jsonData = options.beforeValidation(jsonData);
|
|
84
|
+
}
|
|
85
|
+
return validator(jsonData);
|
|
86
|
+
}
|
|
87
|
+
catch (validationError) {
|
|
88
|
+
if (disableJsonFixer) {
|
|
89
|
+
throw validationError;
|
|
90
|
+
}
|
|
91
|
+
// Attempt a one-time fix for schema validation errors.
|
|
92
|
+
const errorDetails = `Schema Validation Error: ${validationError.message}`;
|
|
93
|
+
const fixedResponse = await _tryToFixJson(JSON.stringify(jsonData, null, 2), schemaJsonString, errorDetails, options);
|
|
94
|
+
if (fixedResponse) {
|
|
95
|
+
try {
|
|
96
|
+
let fixedJsonData = JSON.parse(fixedResponse);
|
|
97
|
+
if (options?.beforeValidation) {
|
|
98
|
+
fixedJsonData = options.beforeValidation(fixedJsonData);
|
|
99
|
+
}
|
|
100
|
+
return validator(fixedJsonData);
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
// Fix-up failed, throw original validation error
|
|
104
|
+
throw validationError;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
throw validationError; // if no fixed response
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function _getJsonPromptConfig(messages, schema, options) {
|
|
111
|
+
const schemaJsonString = JSON.stringify(schema);
|
|
112
|
+
const commonPromptFooter = `
|
|
113
|
+
Your response MUST be a single JSON entity (object or array) that strictly adheres to the following JSON schema.
|
|
114
|
+
Do NOT include any other text, explanations, or markdown formatting (like \`\`\`json) before or after the JSON entity.
|
|
115
|
+
|
|
116
|
+
JSON schema:
|
|
117
|
+
${schemaJsonString}`;
|
|
118
|
+
// Clone messages to avoid mutating the input
|
|
119
|
+
const finalMessages = [...messages];
|
|
120
|
+
// Find the first system message to append instructions to
|
|
121
|
+
const systemMessageIndex = finalMessages.findIndex(m => m.role === 'system');
|
|
122
|
+
if (systemMessageIndex !== -1) {
|
|
123
|
+
// Append to existing system message
|
|
124
|
+
const existingContent = finalMessages[systemMessageIndex].content;
|
|
125
|
+
finalMessages[systemMessageIndex] = {
|
|
126
|
+
...finalMessages[systemMessageIndex],
|
|
127
|
+
content: `${existingContent}\n${commonPromptFooter}`
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
// Prepend new system message
|
|
132
|
+
finalMessages.unshift({
|
|
133
|
+
role: 'system',
|
|
134
|
+
content: commonPromptFooter
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const useResponseFormat = options?.useResponseFormat ?? true;
|
|
138
|
+
const response_format = useResponseFormat
|
|
139
|
+
? { type: 'json_object' }
|
|
140
|
+
: undefined;
|
|
141
|
+
return { finalMessages, schemaJsonString, response_format };
|
|
142
|
+
}
|
|
143
|
+
async function promptJson(messages, schema, validator, options) {
|
|
144
|
+
const { finalMessages, schemaJsonString, response_format } = _getJsonPromptConfig(messages, schema, options);
|
|
145
|
+
const processResponse = async (llmResponseString) => {
|
|
146
|
+
let jsonData;
|
|
147
|
+
try {
|
|
148
|
+
jsonData = await _parseOrFixJson(llmResponseString, schemaJsonString, options);
|
|
149
|
+
}
|
|
150
|
+
catch (parseError) {
|
|
151
|
+
const errorMessage = `Your previous response resulted in an error.
|
|
152
|
+
Error Type: JSON_PARSE_ERROR
|
|
153
|
+
Error Details: ${parseError.message}
|
|
154
|
+
The response provided was not valid JSON. Please correct it.`;
|
|
155
|
+
throw new createLlmRetryClient_js_1.LlmRetryError(errorMessage, 'JSON_PARSE_ERROR', undefined, llmResponseString);
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const validatedData = await _validateOrFix(jsonData, validator, schemaJsonString, options);
|
|
159
|
+
return validatedData;
|
|
160
|
+
}
|
|
161
|
+
catch (validationError) {
|
|
162
|
+
// We assume the validator throws an error with a meaningful message
|
|
163
|
+
const rawResponseForError = JSON.stringify(jsonData, null, 2);
|
|
164
|
+
const errorDetails = validationError.message;
|
|
165
|
+
const errorMessage = `Your previous response resulted in an error.
|
|
166
|
+
Error Type: SCHEMA_VALIDATION_ERROR
|
|
167
|
+
Error Details: ${errorDetails}
|
|
168
|
+
The response was valid JSON but did not conform to the required schema. Please review the errors and the schema to provide a corrected response.`;
|
|
169
|
+
throw new createLlmRetryClient_js_1.LlmRetryError(errorMessage, 'CUSTOM_ERROR', validationError, rawResponseForError);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const retryOptions = {
|
|
173
|
+
...options,
|
|
174
|
+
messages: finalMessages,
|
|
175
|
+
response_format,
|
|
176
|
+
validate: processResponse
|
|
177
|
+
};
|
|
178
|
+
return llmRetryClient.promptTextRetry(retryOptions);
|
|
179
|
+
}
|
|
180
|
+
async function isPromptJsonCached(messages, schema, options) {
|
|
181
|
+
const { finalMessages, response_format } = _getJsonPromptConfig(messages, schema, options);
|
|
182
|
+
const { maxRetries, useResponseFormat: _u, beforeValidation, ...restOptions } = options || {};
|
|
183
|
+
return isPromptCached({
|
|
184
|
+
messages: finalMessages,
|
|
185
|
+
response_format,
|
|
186
|
+
...restOptions
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return { promptJson, isPromptJsonCached };
|
|
190
|
+
}
|
|
@@ -1,28 +1,10 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
2
|
import * as z from "zod";
|
|
3
|
-
import { PromptFunction, LlmPromptOptions, IsPromptCachedFunction } from "./createLlmClient.js";
|
|
4
3
|
import { ZodTypeAny } from "zod";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* If true, passes `response_format: { type: 'json_object' }` to the model.
|
|
9
|
-
* If false, only includes the schema in the system prompt.
|
|
10
|
-
* Defaults to true.
|
|
11
|
-
*/
|
|
12
|
-
useResponseFormat?: boolean;
|
|
13
|
-
/**
|
|
14
|
-
* A hook to process the parsed JSON data before it is validated against the Zod schema.
|
|
15
|
-
* This can be used to merge partial results or perform other transformations.
|
|
16
|
-
* @param data The parsed JSON data from the LLM response.
|
|
17
|
-
* @returns The processed data to be validated.
|
|
18
|
-
*/
|
|
19
|
-
beforeValidation?: (data: any) => any;
|
|
20
|
-
};
|
|
4
|
+
import { JsonSchemaClient, JsonSchemaLlmClientOptions } from "./createJsonSchemaLlmClient.js";
|
|
5
|
+
export type ZodLlmClientOptions = JsonSchemaLlmClientOptions;
|
|
21
6
|
export interface CreateZodLlmClientParams {
|
|
22
|
-
|
|
23
|
-
isPromptCached: IsPromptCachedFunction;
|
|
24
|
-
fallbackPrompt?: PromptFunction;
|
|
25
|
-
disableJsonFixer?: boolean;
|
|
7
|
+
jsonSchemaClient: JsonSchemaClient;
|
|
26
8
|
}
|
|
27
9
|
export interface NormalizedZodArgs<T extends ZodTypeAny> {
|
|
28
10
|
messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[];
|
|
@@ -36,7 +36,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.normalizeZodArgs = normalizeZodArgs;
|
|
37
37
|
exports.createZodLlmClient = createZodLlmClient;
|
|
38
38
|
const z = __importStar(require("zod"));
|
|
39
|
-
const createLlmRetryClient_js_1 = require("./createLlmRetryClient.js");
|
|
40
39
|
const zod_1 = require("zod");
|
|
41
40
|
function isZodSchema(obj) {
|
|
42
41
|
return (typeof obj === 'object' &&
|
|
@@ -89,197 +88,32 @@ function normalizeZodArgs(arg1, arg2, arg3, arg4) {
|
|
|
89
88
|
throw new Error("Invalid arguments passed to promptZod");
|
|
90
89
|
}
|
|
91
90
|
function createZodLlmClient(params) {
|
|
92
|
-
const {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const fixupPrompt = `
|
|
96
|
-
An attempt to generate a JSON object resulted in the following output, which is either not valid JSON or does not conform to the required schema.
|
|
97
|
-
|
|
98
|
-
Your task is to act as a JSON fixer. Analyze the provided "BROKEN RESPONSE" and correct it to match the "REQUIRED JSON SCHEMA".
|
|
99
|
-
|
|
100
|
-
- If the broken response contains all the necessary information to create a valid JSON object according to the schema, please provide the corrected, valid JSON object.
|
|
101
|
-
- If the broken response is missing essential information, or is too garbled to be fixed, please respond with the exact string: "CANNOT_FIX".
|
|
102
|
-
- Your response must be ONLY the corrected JSON object or the string "CANNOT_FIX". Do not include any other text, explanations, or markdown formatting.
|
|
103
|
-
|
|
104
|
-
REQUIRED JSON SCHEMA:
|
|
105
|
-
${schemaJsonString}
|
|
106
|
-
|
|
107
|
-
ERROR DETAILS:
|
|
108
|
-
${errorDetails}
|
|
109
|
-
|
|
110
|
-
BROKEN RESPONSE:
|
|
111
|
-
${brokenResponse}
|
|
112
|
-
`;
|
|
113
|
-
const messages = [
|
|
114
|
-
{ role: 'system', content: 'You are an expert at fixing malformed JSON data to match a specific schema.' },
|
|
115
|
-
{ role: 'user', content: fixupPrompt }
|
|
116
|
-
];
|
|
117
|
-
const useResponseFormat = options?.useResponseFormat ?? true;
|
|
118
|
-
const response_format = useResponseFormat
|
|
119
|
-
? { type: 'json_object' }
|
|
120
|
-
: undefined;
|
|
121
|
-
const { maxRetries, useResponseFormat: _useResponseFormat, ...restOptions } = options || {};
|
|
122
|
-
const completion = await prompt({
|
|
123
|
-
messages,
|
|
124
|
-
response_format,
|
|
125
|
-
...restOptions
|
|
126
|
-
});
|
|
127
|
-
const fixedResponse = completion.choices[0]?.message?.content;
|
|
128
|
-
if (fixedResponse && fixedResponse.trim() === 'CANNOT_FIX') {
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
return fixedResponse || null;
|
|
132
|
-
}
|
|
133
|
-
async function _parseOrFixJson(llmResponseString, schemaJsonString, options) {
|
|
134
|
-
let jsonDataToParse = llmResponseString.trim();
|
|
135
|
-
// Robust handling for responses wrapped in markdown code blocks
|
|
136
|
-
const codeBlockRegex = /```(?:json)?\s*([\s\S]*?)\s*```/;
|
|
137
|
-
const match = codeBlockRegex.exec(jsonDataToParse);
|
|
138
|
-
if (match && match[1]) {
|
|
139
|
-
jsonDataToParse = match[1].trim();
|
|
140
|
-
}
|
|
141
|
-
if (jsonDataToParse === "") {
|
|
142
|
-
throw new Error("LLM returned an empty string.");
|
|
143
|
-
}
|
|
144
|
-
try {
|
|
145
|
-
return JSON.parse(jsonDataToParse);
|
|
146
|
-
}
|
|
147
|
-
catch (parseError) {
|
|
148
|
-
if (disableJsonFixer) {
|
|
149
|
-
throw parseError; // re-throw original error
|
|
150
|
-
}
|
|
151
|
-
// Attempt a one-time fix before failing.
|
|
152
|
-
const errorDetails = `JSON Parse Error: ${parseError.message}`;
|
|
153
|
-
const fixedResponse = await _tryToFixJson(jsonDataToParse, schemaJsonString, errorDetails, options);
|
|
154
|
-
if (fixedResponse) {
|
|
155
|
-
try {
|
|
156
|
-
return JSON.parse(fixedResponse);
|
|
157
|
-
}
|
|
158
|
-
catch (e) {
|
|
159
|
-
// Fix-up failed, throw original error.
|
|
160
|
-
throw parseError;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
throw parseError; // if no fixed response
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
async function _validateOrFixSchema(jsonData, dataExtractionSchema, schemaJsonString, options) {
|
|
167
|
-
try {
|
|
168
|
-
if (options?.beforeValidation) {
|
|
169
|
-
jsonData = options.beforeValidation(jsonData);
|
|
170
|
-
}
|
|
171
|
-
return dataExtractionSchema.parse(jsonData);
|
|
172
|
-
}
|
|
173
|
-
catch (validationError) {
|
|
174
|
-
if (!(validationError instanceof zod_1.ZodError) || disableJsonFixer) {
|
|
175
|
-
throw validationError;
|
|
176
|
-
}
|
|
177
|
-
// Attempt a one-time fix for schema validation errors.
|
|
178
|
-
const errorDetails = `Schema Validation Error: ${JSON.stringify(validationError.format(), null, 2)}`;
|
|
179
|
-
const fixedResponse = await _tryToFixJson(JSON.stringify(jsonData, null, 2), schemaJsonString, errorDetails, options);
|
|
180
|
-
if (fixedResponse) {
|
|
181
|
-
try {
|
|
182
|
-
let fixedJsonData = JSON.parse(fixedResponse);
|
|
183
|
-
if (options?.beforeValidation) {
|
|
184
|
-
fixedJsonData = options.beforeValidation(fixedJsonData);
|
|
185
|
-
}
|
|
186
|
-
return dataExtractionSchema.parse(fixedJsonData);
|
|
187
|
-
}
|
|
188
|
-
catch (e) {
|
|
189
|
-
// Fix-up failed, throw original validation error
|
|
190
|
-
throw validationError;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
throw validationError; // if no fixed response
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
function _getZodPromptConfig(messages, dataExtractionSchema, options) {
|
|
91
|
+
const { jsonSchemaClient } = params;
|
|
92
|
+
async function promptZod(arg1, arg2, arg3, arg4) {
|
|
93
|
+
const { messages, dataExtractionSchema, options } = normalizeZodArgs(arg1, arg2, arg3, arg4);
|
|
197
94
|
const schema = z.toJSONSchema(dataExtractionSchema, {
|
|
198
95
|
unrepresentable: 'any'
|
|
199
96
|
});
|
|
200
|
-
const
|
|
201
|
-
const commonPromptFooter = `
|
|
202
|
-
Your response MUST be a single JSON entity (object or array) that strictly adheres to the following JSON schema.
|
|
203
|
-
Do NOT include any other text, explanations, or markdown formatting (like \`\`\`json) before or after the JSON entity.
|
|
204
|
-
|
|
205
|
-
JSON schema:
|
|
206
|
-
${schemaJsonString}`;
|
|
207
|
-
// Clone messages to avoid mutating the input
|
|
208
|
-
const finalMessages = [...messages];
|
|
209
|
-
// Find the first system message to append instructions to
|
|
210
|
-
const systemMessageIndex = finalMessages.findIndex(m => m.role === 'system');
|
|
211
|
-
if (systemMessageIndex !== -1) {
|
|
212
|
-
// Append to existing system message
|
|
213
|
-
const existingContent = finalMessages[systemMessageIndex].content;
|
|
214
|
-
finalMessages[systemMessageIndex] = {
|
|
215
|
-
...finalMessages[systemMessageIndex],
|
|
216
|
-
content: `${existingContent}\n${commonPromptFooter}`
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
// Prepend new system message
|
|
221
|
-
finalMessages.unshift({
|
|
222
|
-
role: 'system',
|
|
223
|
-
content: commonPromptFooter
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
const useResponseFormat = options?.useResponseFormat ?? true;
|
|
227
|
-
const response_format = useResponseFormat
|
|
228
|
-
? { type: 'json_object' }
|
|
229
|
-
: undefined;
|
|
230
|
-
return { finalMessages, schemaJsonString, response_format };
|
|
231
|
-
}
|
|
232
|
-
async function promptZod(arg1, arg2, arg3, arg4) {
|
|
233
|
-
const { messages, dataExtractionSchema, options } = normalizeZodArgs(arg1, arg2, arg3, arg4);
|
|
234
|
-
const { finalMessages, schemaJsonString, response_format } = _getZodPromptConfig(messages, dataExtractionSchema, options);
|
|
235
|
-
const processResponse = async (llmResponseString) => {
|
|
236
|
-
let jsonData;
|
|
237
|
-
try {
|
|
238
|
-
jsonData = await _parseOrFixJson(llmResponseString, schemaJsonString, options);
|
|
239
|
-
}
|
|
240
|
-
catch (parseError) {
|
|
241
|
-
const errorMessage = `Your previous response resulted in an error.
|
|
242
|
-
Error Type: JSON_PARSE_ERROR
|
|
243
|
-
Error Details: ${parseError.message}
|
|
244
|
-
The response provided was not valid JSON. Please correct it.`;
|
|
245
|
-
throw new createLlmRetryClient_js_1.LlmRetryError(errorMessage, 'JSON_PARSE_ERROR', undefined, llmResponseString);
|
|
246
|
-
}
|
|
97
|
+
const validator = (data) => {
|
|
247
98
|
try {
|
|
248
|
-
|
|
249
|
-
return validatedData;
|
|
99
|
+
return dataExtractionSchema.parse(data);
|
|
250
100
|
}
|
|
251
|
-
catch (
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const errorMessage = `Your previous response resulted in an error.
|
|
256
|
-
Error Type: SCHEMA_VALIDATION_ERROR
|
|
257
|
-
Error Details: ${errorDetails}
|
|
258
|
-
The response was valid JSON but did not conform to the required schema. Please review the errors and the schema to provide a corrected response.`;
|
|
259
|
-
throw new createLlmRetryClient_js_1.LlmRetryError(errorMessage, 'CUSTOM_ERROR', validationError.format(), rawResponseForError);
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (error instanceof zod_1.ZodError) {
|
|
103
|
+
// Format the error nicely for the LLM
|
|
104
|
+
throw new Error(JSON.stringify(error.format(), null, 2));
|
|
260
105
|
}
|
|
261
|
-
|
|
262
|
-
throw validationError;
|
|
106
|
+
throw error;
|
|
263
107
|
}
|
|
264
108
|
};
|
|
265
|
-
|
|
266
|
-
...options,
|
|
267
|
-
messages: finalMessages,
|
|
268
|
-
response_format,
|
|
269
|
-
validate: processResponse
|
|
270
|
-
};
|
|
271
|
-
// Use promptTextRetry because we expect a string response to parse as JSON
|
|
272
|
-
return llmRetryClient.promptTextRetry(retryOptions);
|
|
109
|
+
return jsonSchemaClient.promptJson(messages, schema, validator, options);
|
|
273
110
|
}
|
|
274
111
|
async function isPromptZodCached(arg1, arg2, arg3, arg4) {
|
|
275
112
|
const { messages, dataExtractionSchema, options } = normalizeZodArgs(arg1, arg2, arg3, arg4);
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
return isPromptCached({
|
|
279
|
-
messages: finalMessages,
|
|
280
|
-
response_format,
|
|
281
|
-
...restOptions
|
|
113
|
+
const schema = z.toJSONSchema(dataExtractionSchema, {
|
|
114
|
+
unrepresentable: 'any'
|
|
282
115
|
});
|
|
116
|
+
return jsonSchemaClient.isPromptJsonCached(messages, schema, options);
|
|
283
117
|
}
|
|
284
118
|
return { promptZod, isPromptZodCached };
|
|
285
119
|
}
|
|
@@ -79,6 +79,19 @@ const createZodLlmClient_js_1 = require("./createZodLlmClient.js");
|
|
|
79
79
|
(0, vitest_1.expect)(result.dataExtractionSchema).toBe(TestSchema);
|
|
80
80
|
(0, vitest_1.expect)(result.options).toBe(options);
|
|
81
81
|
});
|
|
82
|
+
(0, vitest_1.it)('should normalize Messages Array with few-shot examples + Schema (Case 0)', () => {
|
|
83
|
+
const messages = [
|
|
84
|
+
{ role: 'system', content: 'Extract sentiment.' },
|
|
85
|
+
{ role: 'user', content: 'I love this!' },
|
|
86
|
+
{ role: 'assistant', content: JSON.stringify({ sentiment: 'positive' }) },
|
|
87
|
+
{ role: 'user', content: 'I hate this.' }
|
|
88
|
+
];
|
|
89
|
+
const Schema = zod_1.z.object({ sentiment: zod_1.z.enum(['positive', 'negative']) });
|
|
90
|
+
const result = (0, createZodLlmClient_js_1.normalizeZodArgs)(messages, Schema);
|
|
91
|
+
(0, vitest_1.expect)(result.messages).toBe(messages);
|
|
92
|
+
(0, vitest_1.expect)(result.dataExtractionSchema).toBe(Schema);
|
|
93
|
+
(0, vitest_1.expect)(result.options).toBeUndefined();
|
|
94
|
+
});
|
|
82
95
|
(0, vitest_1.it)('should throw error for invalid arguments', () => {
|
|
83
96
|
(0, vitest_1.expect)(() => (0, createZodLlmClient_js_1.normalizeZodArgs)({})).toThrow('Invalid arguments');
|
|
84
97
|
});
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -17,5 +17,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
__exportStar(require("./createLlmClient.js"), exports);
|
|
18
18
|
__exportStar(require("./createLlmRetryClient.js"), exports);
|
|
19
19
|
__exportStar(require("./createZodLlmClient.js"), exports);
|
|
20
|
+
__exportStar(require("./createJsonSchemaLlmClient.js"), exports);
|
|
20
21
|
__exportStar(require("./llmFactory.js"), exports);
|
|
21
22
|
__exportStar(require("./retryUtils.js"), exports);
|
package/dist/llmFactory.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export declare function createLlm(params: CreateLlmFactoryParams): {
|
|
|
14
14
|
<T extends import("zod").ZodType>(prompt: string, schema: T, options?: import("./createZodLlmClient.js").ZodLlmClientOptions): Promise<boolean>;
|
|
15
15
|
<T extends import("zod").ZodType>(mainInstruction: string, userMessagePayload: string | import("openai/resources/index.js").ChatCompletionContentPart[], dataExtractionSchema: T, options?: import("./createZodLlmClient.js").ZodLlmClientOptions): Promise<boolean>;
|
|
16
16
|
};
|
|
17
|
+
promptJson: <T>(messages: import("openai/resources/index.js").ChatCompletionMessageParam[], schema: Record<string, any>, validator: (data: any) => T, options?: import("./createJsonSchemaLlmClient.js").JsonSchemaLlmClientOptions) => Promise<T>;
|
|
18
|
+
isPromptJsonCached: (messages: import("openai/resources/index.js").ChatCompletionMessageParam[], schema: Record<string, any>, options?: import("./createJsonSchemaLlmClient.js").JsonSchemaLlmClientOptions) => Promise<boolean>;
|
|
17
19
|
promptRetry: {
|
|
18
20
|
<T = import("openai/resources/index.js").ChatCompletion>(content: string, options?: Omit<import("./createLlmRetryClient.js").LlmRetryOptions<T>, "messages">): Promise<T>;
|
|
19
21
|
<T = import("openai/resources/index.js").ChatCompletion>(options: import("./createLlmRetryClient.js").LlmRetryOptions<T>): Promise<T>;
|
package/dist/llmFactory.js
CHANGED
|
@@ -4,18 +4,23 @@ exports.createLlm = createLlm;
|
|
|
4
4
|
const createLlmClient_js_1 = require("./createLlmClient.js");
|
|
5
5
|
const createLlmRetryClient_js_1 = require("./createLlmRetryClient.js");
|
|
6
6
|
const createZodLlmClient_js_1 = require("./createZodLlmClient.js");
|
|
7
|
+
const createJsonSchemaLlmClient_js_1 = require("./createJsonSchemaLlmClient.js");
|
|
7
8
|
function createLlm(params) {
|
|
8
9
|
const baseClient = (0, createLlmClient_js_1.createLlmClient)(params);
|
|
9
10
|
const retryClient = (0, createLlmRetryClient_js_1.createLlmRetryClient)({
|
|
10
11
|
prompt: baseClient.prompt
|
|
11
12
|
});
|
|
12
|
-
const
|
|
13
|
+
const jsonSchemaClient = (0, createJsonSchemaLlmClient_js_1.createJsonSchemaLlmClient)({
|
|
13
14
|
prompt: baseClient.prompt,
|
|
14
15
|
isPromptCached: baseClient.isPromptCached
|
|
15
16
|
});
|
|
17
|
+
const zodClient = (0, createZodLlmClient_js_1.createZodLlmClient)({
|
|
18
|
+
jsonSchemaClient
|
|
19
|
+
});
|
|
16
20
|
return {
|
|
17
21
|
...baseClient,
|
|
18
22
|
...retryClient,
|
|
23
|
+
...jsonSchemaClient,
|
|
19
24
|
...zodClient
|
|
20
25
|
};
|
|
21
26
|
}
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -115,47 +115,50 @@ const buffer2 = await llm.promptImage({
|
|
|
115
115
|
|
|
116
116
|
---
|
|
117
117
|
|
|
118
|
-
# Use Case 3: Structured Data (`llm.promptZod`)
|
|
118
|
+
# Use Case 3: Structured Data (`llm.promptJson` & `llm.promptZod`)
|
|
119
119
|
|
|
120
|
-
This is a high-level wrapper that employs a **Re-asking Loop**. If the LLM outputs invalid JSON or data that fails the
|
|
120
|
+
This is a high-level wrapper that employs a **Re-asking Loop**. If the LLM outputs invalid JSON or data that fails the schema validation, the client automatically feeds the error back to the LLM and asks it to fix it (up to `maxRetries`).
|
|
121
121
|
|
|
122
|
-
**Return Type:** `Promise<
|
|
122
|
+
**Return Type:** `Promise<T>`
|
|
123
|
+
|
|
124
|
+
### Level 1: Raw JSON Schema (`promptJson`)
|
|
125
|
+
Use this if you have a standard JSON Schema object (e.g. from another library or API) and don't want to use Zod.
|
|
123
126
|
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
```typescript
|
|
128
|
+
const MySchema = {
|
|
129
|
+
type: "object",
|
|
130
|
+
properties: {
|
|
131
|
+
sentiment: { type: "string", enum: ["positive", "negative"] },
|
|
132
|
+
score: { type: "number" }
|
|
133
|
+
},
|
|
134
|
+
required: ["sentiment", "score"],
|
|
135
|
+
additionalProperties: false
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const result = await llm.promptJson(
|
|
139
|
+
[{ role: "user", content: "I love this!" }],
|
|
140
|
+
MySchema,
|
|
141
|
+
(data) => data // Optional validator function
|
|
142
|
+
);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Level 2: Zod Wrapper (`promptZod`)
|
|
146
|
+
This is syntactic sugar over `promptJson`. It converts your Zod schema to JSON Schema and automatically sets up the validator to throw formatted Zod errors for the retry loop.
|
|
147
|
+
|
|
148
|
+
**Return Type:** `Promise<z.infer<typeof Schema>>`
|
|
126
149
|
|
|
127
150
|
```typescript
|
|
128
151
|
import { z } from 'zod';
|
|
129
152
|
const UserSchema = z.object({ name: z.string(), age: z.number() });
|
|
130
153
|
|
|
131
|
-
//
|
|
154
|
+
// 1. Schema Only (Hallucinate data)
|
|
132
155
|
const user = await llm.promptZod(UserSchema);
|
|
133
|
-
// Output: { name: "Alice", age: 32 }
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
### Level 2: Extraction (Injection Shortcuts)
|
|
137
|
-
Pass context alongside the schema. This automates the "System Prompt JSON Injection".
|
|
138
156
|
|
|
139
|
-
|
|
140
|
-
// 1. Extract from String
|
|
157
|
+
// 2. Extraction (Context + Schema)
|
|
141
158
|
const email = "Meeting at 2 PM with Bob.";
|
|
142
159
|
const event = await llm.promptZod(email, z.object({ time: z.string(), who: z.string() }));
|
|
143
160
|
|
|
144
|
-
//
|
|
145
|
-
// Useful for auditing code or translations where instructions must not bleed into data.
|
|
146
|
-
const analysis = await llm.promptZod(
|
|
147
|
-
"You are a security auditor.", // Arg 1: System
|
|
148
|
-
"function dangerous() {}", // Arg 2: User Data
|
|
149
|
-
SecuritySchema // Arg 3: Schema
|
|
150
|
-
);
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
### Level 3: State & Options (History + Config)
|
|
154
|
-
Process full chat history into state, and use the **Options Object (4th Argument)** to control the internals (Models, Retries, Caching).
|
|
155
|
-
|
|
156
|
-
**Input Type:** `ZodLlmClientOptions`
|
|
157
|
-
|
|
158
|
-
```typescript
|
|
161
|
+
// 3. Full Control (History + Schema + Options)
|
|
159
162
|
const history = [
|
|
160
163
|
{ role: "user", content: "I cast Fireball." },
|
|
161
164
|
{ role: "assistant", content: "It misses." }
|
|
@@ -173,12 +176,12 @@ const gameState = await llm.promptZod(
|
|
|
173
176
|
);
|
|
174
177
|
```
|
|
175
178
|
|
|
176
|
-
### Level
|
|
177
|
-
Sometimes LLMs output data that is *almost* correct (e.g., strings for numbers). You can sanitize data before
|
|
179
|
+
### Level 3: Hooks & Pre-processing
|
|
180
|
+
Sometimes LLMs output data that is *almost* correct (e.g., strings for numbers). You can sanitize data before validation runs.
|
|
178
181
|
|
|
179
182
|
```typescript
|
|
180
183
|
const result = await llm.promptZod(MySchema, {
|
|
181
|
-
// Transform JSON before
|
|
184
|
+
// Transform JSON before validation runs
|
|
182
185
|
beforeValidation: (data) => {
|
|
183
186
|
if (data.price && typeof data.price === 'string') {
|
|
184
187
|
return { ...data, price: parseFloat(data.price) };
|
|
@@ -187,7 +190,6 @@ const result = await llm.promptZod(MySchema, {
|
|
|
187
190
|
},
|
|
188
191
|
|
|
189
192
|
// Toggle usage of 'response_format: { type: "json_object" }'
|
|
190
|
-
// Sometimes strict JSON mode is too restrictive for creative tasks
|
|
191
193
|
useResponseFormat: false
|
|
192
194
|
});
|
|
193
195
|
```
|