doc-detective-common 3.6.0 → 3.7.0-preview.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/dist/schemas/config_v3.schema.json +3880 -40
- package/dist/schemas/report_v3.schema.json +3991 -69
- package/dist/schemas/resolvedTests_v3.schema.json +7925 -163
- package/dist/schemas/sourceLocation_v3.schema.json +106 -0
- package/dist/schemas/spec_v3.schema.json +3991 -69
- package/dist/schemas/step_v3.schema.json +1915 -7
- package/dist/schemas/test_v3.schema.json +3995 -73
- package/package.json +6 -2
- package/src/ai.js +532 -0
- package/src/index.js +8 -0
- package/src/refineStep.js +267 -0
- package/src/schemas/build/checkLink_v2.schema.json +1 -1
- package/src/schemas/build/checkLink_v3.schema.json +1 -1
- package/src/schemas/build/click_v3.schema.json +1 -1
- package/src/schemas/build/config_v2.schema.json +14 -14
- package/src/schemas/build/config_v3.schema.json +30 -6
- package/src/schemas/build/context_v2.schema.json +1 -1
- package/src/schemas/build/context_v3.schema.json +1 -1
- package/src/schemas/build/dragAndDrop_v3.schema.json +1 -1
- package/src/schemas/build/find_v2.schema.json +3 -3
- package/src/schemas/build/find_v3.schema.json +4 -4
- package/src/schemas/build/goTo_v2.schema.json +1 -1
- package/src/schemas/build/goTo_v3.schema.json +1 -1
- package/src/schemas/build/httpRequest_v2.schema.json +2 -2
- package/src/schemas/build/httpRequest_v3.schema.json +3 -3
- package/src/schemas/build/loadCookie_v3.schema.json +1 -1
- package/src/schemas/build/loadVariables_v3.schema.json +1 -1
- package/src/schemas/build/moveTo_v2.schema.json +1 -1
- package/src/schemas/build/openApi_v2.schema.json +1 -1
- package/src/schemas/build/openApi_v3.schema.json +1 -1
- package/src/schemas/build/record_v3.schema.json +1 -1
- package/src/schemas/build/report_v3.schema.json +5 -5
- package/src/schemas/build/resolvedTests_v3.schema.json +3 -3
- package/src/schemas/build/runCode_v2.schema.json +1 -1
- package/src/schemas/build/runCode_v3.schema.json +1 -1
- package/src/schemas/build/runShell_v2.schema.json +1 -1
- package/src/schemas/build/runShell_v3.schema.json +1 -1
- package/src/schemas/build/saveCookie_v3.schema.json +1 -1
- package/src/schemas/build/saveScreenshot_v2.schema.json +1 -1
- package/src/schemas/build/screenshot_v3.schema.json +1 -1
- package/src/schemas/build/setVariables_v2.schema.json +1 -1
- package/src/schemas/build/sourceLocation_v3.schema.json +107 -0
- package/src/schemas/build/spec_v2.schema.json +4 -4
- package/src/schemas/build/spec_v3.schema.json +4 -4
- package/src/schemas/build/startRecording_v2.schema.json +1 -1
- package/src/schemas/build/step_v3.schema.json +45 -38
- package/src/schemas/build/stopRecord_v3.schema.json +1 -1
- package/src/schemas/build/stopRecording_v2.schema.json +1 -1
- package/src/schemas/build/test_v2.schema.json +15 -15
- package/src/schemas/build/test_v3.schema.json +10 -6
- package/src/schemas/build/typeKeys_v2.schema.json +1 -1
- package/src/schemas/build/type_v3.schema.json +1 -1
- package/src/schemas/build/wait_v2.schema.json +1 -1
- package/src/schemas/build/wait_v3.schema.json +1 -1
- package/src/schemas/dereferenceSchemas.js +1 -0
- package/src/schemas/output_schemas/config_v3.schema.json +3880 -40
- package/src/schemas/output_schemas/report_v3.schema.json +3991 -69
- package/src/schemas/output_schemas/resolvedTests_v3.schema.json +7925 -163
- package/src/schemas/output_schemas/sourceLocation_v3.schema.json +106 -0
- package/src/schemas/output_schemas/spec_v3.schema.json +3991 -69
- package/src/schemas/output_schemas/step_v3.schema.json +1915 -7
- package/src/schemas/output_schemas/test_v3.schema.json +3995 -73
- package/src/schemas/schemas.json +25978 -596
- package/src/schemas/src_schemas/config_v3.schema.json +24 -0
- package/src/schemas/src_schemas/sourceLocation_v3.schema.json +106 -0
- package/src/schemas/src_schemas/step_v3.schema.json +7 -0
- package/src/schemas/src_schemas/test_v3.schema.json +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doc-detective-common",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0-preview.0",
|
|
4
4
|
"description": "Shared components for Doc Detective projects.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -25,12 +25,16 @@
|
|
|
25
25
|
"sinon": "^21.0.0"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
+
"@ai-sdk/anthropic": "^2.0.54",
|
|
29
|
+
"@ai-sdk/openai": "^2.0.80",
|
|
28
30
|
"@apidevtools/json-schema-ref-parser": "^15.1.3",
|
|
31
|
+
"ai": "^5.0.109",
|
|
29
32
|
"ajv": "^8.17.1",
|
|
30
33
|
"ajv-errors": "^3.0.0",
|
|
31
34
|
"ajv-formats": "^3.0.1",
|
|
32
35
|
"ajv-keywords": "^5.1.0",
|
|
33
36
|
"axios": "^1.13.2",
|
|
34
|
-
"yaml": "^2.8.2"
|
|
37
|
+
"yaml": "^2.8.2",
|
|
38
|
+
"zod": "^4.1.13"
|
|
35
39
|
}
|
|
36
40
|
}
|
package/src/ai.js
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
const { generateText, generateObject, jsonSchema } = require("ai");
|
|
2
|
+
const { createOpenAI } = require("@ai-sdk/openai");
|
|
3
|
+
const { createAnthropic } = require("@ai-sdk/anthropic");
|
|
4
|
+
const { z } = require("zod");
|
|
5
|
+
const Ajv = require("ajv");
|
|
6
|
+
const addFormats = require("ajv-formats");
|
|
7
|
+
|
|
8
|
+
const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
|
|
9
|
+
const MAX_SCHEMA_VALIDATION_RETRIES = 3;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Maps our supported model enums to the model identifiers that platforms expect.
|
|
13
|
+
*/
|
|
14
|
+
const modelMap = {
|
|
15
|
+
// Anthropic models
|
|
16
|
+
"anthropic/claude-haiku-4.5": "claude-haiku-4-5",
|
|
17
|
+
"anthropic/claude-sonnet-4.5": "claude-sonnet-4-5",
|
|
18
|
+
"anthropic/claude-opus-4.5": "claude-opus-4-5",
|
|
19
|
+
// OpenAI models
|
|
20
|
+
"openai/gpt-5.1": "gpt-5.1",
|
|
21
|
+
"openai/gpt-5-mini": "gpt-5-mini",
|
|
22
|
+
"openai/gpt-5-nano": "gpt-5-nano",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const getDefaultProvider = (config) => {
|
|
26
|
+
// Try to detect from environment variables if no model is provided
|
|
27
|
+
if (process.env.ANTHROPIC_API_KEY || config.integrations?.anthropic) {
|
|
28
|
+
return {
|
|
29
|
+
provider: "anthropic",
|
|
30
|
+
model: "claude-haiku-4-5",
|
|
31
|
+
apiKey: process.env.ANTHROPIC_API_KEY || config.integrations.anthropic.apiKey,
|
|
32
|
+
};
|
|
33
|
+
} else if (process.env.OPENAI_API_KEY || config.integrations?.openAi) {
|
|
34
|
+
return {
|
|
35
|
+
provider: "openai",
|
|
36
|
+
model: "gpt-5-mini",
|
|
37
|
+
apiKey: process.env.OPENAI_API_KEY || config.integrations.openAi.apiKey,
|
|
38
|
+
};
|
|
39
|
+
} else {
|
|
40
|
+
return { provider: null, model: null, apiKey: null };
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Detects the provider, model, and API from a model string and environment variables.
|
|
46
|
+
* @param {string} model - The model identifier.
|
|
47
|
+
* @returns {{ provider: "openai" | "anthropic" | null, model: string | null, apiKey: string | null }} The detected provider, model, and API key.
|
|
48
|
+
*/
|
|
49
|
+
const detectProvider = (config, model) => {
|
|
50
|
+
const detectedModel = modelMap[model] || null;
|
|
51
|
+
if (!detectedModel) return getDefaultProvider(config);
|
|
52
|
+
|
|
53
|
+
if (model.startsWith("anthropic/") && (process.env.ANTHROPIC_API_KEY || config.integrations?.anthropic)) {
|
|
54
|
+
const apiKey = process.env.ANTHROPIC_API_KEY || config.integrations.anthropic.apiKey;
|
|
55
|
+
return { provider: "anthropic", model: detectedModel, apiKey };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (model.startsWith("openai/") && (process.env.OPENAI_API_KEY || config.integrations?.openAi)) {
|
|
59
|
+
const apiKey = process.env.OPENAI_API_KEY || config.integrations.openAi.apiKey;
|
|
60
|
+
return { provider: "openai", model: detectedModel, apiKey };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { provider: null, model: null };
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Creates a provider instance based on the provider name.
|
|
68
|
+
* @param {Object} options
|
|
69
|
+
* @param {"openai" | "anthropic"} options.provider - The provider name.
|
|
70
|
+
* @param {string} [options.apiKey] - Optional API key override.
|
|
71
|
+
* @param {string} [options.baseURL] - Optional base URL override.
|
|
72
|
+
* @returns {Function} The provider factory function.
|
|
73
|
+
*/
|
|
74
|
+
const createProvider = ({ provider, apiKey, baseURL }) => {
|
|
75
|
+
if (provider === "openai") {
|
|
76
|
+
const options = {};
|
|
77
|
+
if (apiKey) options.apiKey = apiKey;
|
|
78
|
+
if (baseURL) options.baseURL = baseURL;
|
|
79
|
+
return createOpenAI(options);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (provider === "anthropic") {
|
|
83
|
+
const options = {};
|
|
84
|
+
if (apiKey) options.apiKey = apiKey;
|
|
85
|
+
if (baseURL) options.baseURL = baseURL;
|
|
86
|
+
return createAnthropic(options);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
throw new Error(`Unsupported provider: ${provider}`);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Converts a file object to AI SDK image part format.
|
|
94
|
+
* @param {Object} file - The file object.
|
|
95
|
+
* @param {string} file.type - The file type (e.g., "image").
|
|
96
|
+
* @param {string} file.data - Base64 data or URL.
|
|
97
|
+
* @param {string} [file.mimeType] - The MIME type (e.g., "image/png").
|
|
98
|
+
* @returns {Object} The AI SDK image part.
|
|
99
|
+
*/
|
|
100
|
+
const fileToImagePart = (file) => {
|
|
101
|
+
if (file.type !== "image") {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Unsupported file type: ${file.type}. Only "image" is supported.`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check if data is a URL
|
|
108
|
+
const isUrl =
|
|
109
|
+
file.data.startsWith("http://") || file.data.startsWith("https://");
|
|
110
|
+
|
|
111
|
+
if (isUrl) {
|
|
112
|
+
return {
|
|
113
|
+
type: "image",
|
|
114
|
+
image: new URL(file.data),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Base64 data
|
|
119
|
+
return {
|
|
120
|
+
type: "image",
|
|
121
|
+
image: file.data,
|
|
122
|
+
mimeType: file.mimeType,
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Builds message content from prompt and files.
|
|
128
|
+
* @param {Object} options
|
|
129
|
+
* @param {string} options.prompt - The text prompt.
|
|
130
|
+
* @param {Array} [options.files] - Optional array of file objects.
|
|
131
|
+
* @returns {string | Array} The message content.
|
|
132
|
+
*/
|
|
133
|
+
const buildMessageContent = ({ prompt, files }) => {
|
|
134
|
+
if (!files || files.length === 0) {
|
|
135
|
+
return prompt;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const parts = [];
|
|
139
|
+
|
|
140
|
+
// Add text part
|
|
141
|
+
parts.push({ type: "text", text: prompt });
|
|
142
|
+
|
|
143
|
+
// Add file parts
|
|
144
|
+
for (const file of files) {
|
|
145
|
+
parts.push(fileToImagePart(file));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return parts;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Checks if a schema is a Zod schema.
|
|
153
|
+
* @param {Object} schema - The schema to check.
|
|
154
|
+
* @returns {boolean} True if the schema is a Zod schema.
|
|
155
|
+
*/
|
|
156
|
+
const isZodSchema = (schema) => {
|
|
157
|
+
return schema && typeof schema.safeParse === "function";
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Validates an object against a Zod schema.
|
|
162
|
+
* @param {Object} object - The object to validate.
|
|
163
|
+
* @param {z.ZodSchema} schema - The Zod schema.
|
|
164
|
+
* @returns {{ valid: boolean, errors: string | null, object: Object }} Validation result.
|
|
165
|
+
*/
|
|
166
|
+
const validateAgainstZodSchema = (object, schema) => {
|
|
167
|
+
const result = schema.safeParse(object);
|
|
168
|
+
|
|
169
|
+
if (result.success) {
|
|
170
|
+
return { valid: true, errors: null, object: result.data };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const errors = result.error.issues
|
|
174
|
+
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
175
|
+
.join(", ");
|
|
176
|
+
|
|
177
|
+
return { valid: false, errors, object };
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Validates an object against a JSON schema.
|
|
182
|
+
* @param {Object} object - The object to validate.
|
|
183
|
+
* @param {Object} schema - The JSON schema.
|
|
184
|
+
* @returns {{ valid: boolean, errors: string | null, object: Object }} Validation result.
|
|
185
|
+
*/
|
|
186
|
+
const validateAgainstJsonSchema = (object, schema) => {
|
|
187
|
+
const ajv = new Ajv({
|
|
188
|
+
allErrors: true,
|
|
189
|
+
useDefaults: true,
|
|
190
|
+
coerceTypes: true,
|
|
191
|
+
strict: false,
|
|
192
|
+
});
|
|
193
|
+
addFormats(ajv);
|
|
194
|
+
|
|
195
|
+
const validate = ajv.compile(schema);
|
|
196
|
+
const valid = validate(object);
|
|
197
|
+
|
|
198
|
+
if (valid) {
|
|
199
|
+
return { valid: true, errors: null, object };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const errors = validate.errors
|
|
203
|
+
.map((error) => `${error.instancePath || "/"} ${error.message}`)
|
|
204
|
+
.join(", ");
|
|
205
|
+
|
|
206
|
+
return { valid: false, errors, object };
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Validates an object against a schema (Zod or JSON schema).
|
|
211
|
+
* @param {Object} object - The object to validate.
|
|
212
|
+
* @param {z.ZodSchema | Object} schema - The Zod or JSON schema.
|
|
213
|
+
* @returns {{ valid: boolean, errors: string | null, object: Object }} Validation result.
|
|
214
|
+
*/
|
|
215
|
+
const validateAgainstSchema = (object, schema) => {
|
|
216
|
+
if (isZodSchema(schema)) {
|
|
217
|
+
return validateAgainstZodSchema(object, schema);
|
|
218
|
+
}
|
|
219
|
+
return validateAgainstJsonSchema(object, schema);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Converts a schema to the format expected by the AI SDK.
|
|
224
|
+
* Zod schemas are passed directly; JSON schemas are wrapped with jsonSchema().
|
|
225
|
+
* @param {z.ZodSchema | Object} schema - The Zod or JSON schema.
|
|
226
|
+
* @returns {Object} The schema in AI SDK format.
|
|
227
|
+
*/
|
|
228
|
+
const toAiSdkSchema = (schema) => {
|
|
229
|
+
if (isZodSchema(schema)) {
|
|
230
|
+
return schema;
|
|
231
|
+
}
|
|
232
|
+
return jsonSchema(schema);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Extracts the API key for a provider from a Doc Detective config object.
|
|
237
|
+
* @param {Object} config - The Doc Detective configuration object.
|
|
238
|
+
* @param {"openai" | "anthropic"} provider - The provider name.
|
|
239
|
+
* @returns {string | undefined} The API key if found.
|
|
240
|
+
*/
|
|
241
|
+
const getApiKey = (config, provider) => {
|
|
242
|
+
if (!config || !config.integrations) return undefined;
|
|
243
|
+
|
|
244
|
+
if (
|
|
245
|
+
provider === "anthropic" &&
|
|
246
|
+
(process.env.ANTHROPIC_API_KEY || config.integrations.anthropic)
|
|
247
|
+
) {
|
|
248
|
+
return (
|
|
249
|
+
process.env.ANTHROPIC_API_KEY || config.integrations.anthropic.apiKey
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (
|
|
254
|
+
provider === "openai" &&
|
|
255
|
+
(process.env.OPENAI_API_KEY || config.integrations.openai)
|
|
256
|
+
) {
|
|
257
|
+
return process.env.OPENAI_API_KEY || config.integrations.openai.apiKey;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return undefined;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Generates text or structured output using an AI model.
|
|
265
|
+
*
|
|
266
|
+
* @param {Object} options - Generation options.
|
|
267
|
+
* @param {string} [options.prompt] - The text prompt (required if messages not provided).
|
|
268
|
+
* @param {Array} [options.messages] - Array of messages for multi-turn conversation.
|
|
269
|
+
* @param {Array} [options.files] - Array of file objects to include (e.g., images).
|
|
270
|
+
* @param {string} [options.files[].type] - File type ("image").
|
|
271
|
+
* @param {string} [options.files[].data] - Base64 data or URL.
|
|
272
|
+
* @param {string} [options.files[].mimeType] - MIME type (e.g., "image/png").
|
|
273
|
+
* @param {string} [options.model] - Model identifier (default: "anthropic/claude-haiku-4.5").
|
|
274
|
+
* @param {string} [options.system] - System message.
|
|
275
|
+
* @param {z.ZodSchema | Object} [options.schema] - Zod schema or JSON schema for structured output.
|
|
276
|
+
* @param {string} [options.schemaName] - Name for the schema (used in API calls).
|
|
277
|
+
* @param {string} [options.schemaDescription] - Description for the schema.
|
|
278
|
+
* @param {"openai" | "anthropic"} [options.provider] - Explicit provider override.
|
|
279
|
+
* @param {Object} [options.config] - Doc Detective config object with integrations.anthropic/openai API keys.
|
|
280
|
+
* @param {string} [options.apiKey] - API key override (takes precedence over config and env vars).
|
|
281
|
+
* @param {string} [options.baseURL] - Base URL override for the provider.
|
|
282
|
+
* @param {number} [options.temperature] - Temperature for generation.
|
|
283
|
+
* @param {number} [options.maxTokens] - Maximum tokens to generate.
|
|
284
|
+
* @returns {Promise<Object>} Generation result.
|
|
285
|
+
* @returns {string} [result.text] - Generated text (when no schema provided).
|
|
286
|
+
* @returns {Object} [result.object] - Generated object (when schema provided).
|
|
287
|
+
* @returns {Object} result.usage - Token usage information.
|
|
288
|
+
* @returns {string} result.finishReason - Why generation stopped.
|
|
289
|
+
*
|
|
290
|
+
* @throws {Error} If prompt/messages is missing or provider cannot be determined.
|
|
291
|
+
*/
|
|
292
|
+
const generate = async ({
|
|
293
|
+
prompt,
|
|
294
|
+
messages,
|
|
295
|
+
files,
|
|
296
|
+
model,
|
|
297
|
+
system,
|
|
298
|
+
schema,
|
|
299
|
+
schemaName,
|
|
300
|
+
schemaDescription,
|
|
301
|
+
provider,
|
|
302
|
+
config = {},
|
|
303
|
+
apiKey,
|
|
304
|
+
baseURL,
|
|
305
|
+
temperature,
|
|
306
|
+
maxTokens,
|
|
307
|
+
}) => {
|
|
308
|
+
// Validate required input
|
|
309
|
+
if (!prompt && (!messages || messages.length === 0)) {
|
|
310
|
+
throw new Error("Either 'prompt' or 'messages' is required.");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Determine provider, model, and API key
|
|
314
|
+
const detected = detectProvider(config, model);
|
|
315
|
+
|
|
316
|
+
if (!detected.provider) {
|
|
317
|
+
throw new Error(
|
|
318
|
+
`Cannot determine provider for model "${model}". Please specify a 'provider' option ("openai" or "anthropic").`
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Create provider instance
|
|
323
|
+
const providerFactory = createProvider({
|
|
324
|
+
provider: detected.provider,
|
|
325
|
+
apiKey: detected.apiKey,
|
|
326
|
+
baseURL,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Get model instance
|
|
330
|
+
const modelInstance = providerFactory(detected.model);
|
|
331
|
+
|
|
332
|
+
// Build generation options
|
|
333
|
+
const generationOptions = {
|
|
334
|
+
model: modelInstance,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// Add system message if provided
|
|
338
|
+
if (system) {
|
|
339
|
+
generationOptions.system = system;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Add temperature if provided
|
|
343
|
+
if (temperature !== undefined) {
|
|
344
|
+
generationOptions.temperature = temperature;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Add maxTokens if provided
|
|
348
|
+
if (maxTokens !== undefined) {
|
|
349
|
+
generationOptions.maxTokens = maxTokens;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Build messages or prompt
|
|
353
|
+
if (messages && messages.length > 0) {
|
|
354
|
+
// Find the index of the last user message
|
|
355
|
+
const lastUserIndex = messages.findLastIndex((msg) => msg.role === "user");
|
|
356
|
+
|
|
357
|
+
// Use messages array, attaching files only to the last user message
|
|
358
|
+
generationOptions.messages = messages.map((msg, index) => {
|
|
359
|
+
if (index === lastUserIndex && files && files.length > 0) {
|
|
360
|
+
return {
|
|
361
|
+
...msg,
|
|
362
|
+
content: buildMessageContent({ prompt: msg.content, files }),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return msg;
|
|
366
|
+
});
|
|
367
|
+
} else if (files && files.length > 0) {
|
|
368
|
+
// When files are provided, we must use messages format for multimodal content
|
|
369
|
+
generationOptions.messages = [
|
|
370
|
+
{
|
|
371
|
+
role: "user",
|
|
372
|
+
content: buildMessageContent({ prompt, files }),
|
|
373
|
+
},
|
|
374
|
+
];
|
|
375
|
+
} else {
|
|
376
|
+
// Use simple prompt for text-only requests
|
|
377
|
+
generationOptions.prompt = prompt;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Handle structured output with schema
|
|
381
|
+
if (schema) {
|
|
382
|
+
return generateWithSchemaValidation({
|
|
383
|
+
generationOptions,
|
|
384
|
+
schema,
|
|
385
|
+
schemaName,
|
|
386
|
+
schemaDescription,
|
|
387
|
+
prompt,
|
|
388
|
+
messages,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Generate text
|
|
393
|
+
const result = await generateText(generationOptions);
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
text: result.text,
|
|
397
|
+
usage: result.usage,
|
|
398
|
+
finishReason: result.finishReason,
|
|
399
|
+
};
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Generates structured output with schema validation and retry logic.
|
|
404
|
+
* @param {Object} options
|
|
405
|
+
* @param {Object} options.generationOptions - AI SDK generation options.
|
|
406
|
+
* @param {z.ZodSchema | Object} options.schema - Zod schema or JSON schema for validation.
|
|
407
|
+
* @param {string} [options.schemaName] - Name for the schema.
|
|
408
|
+
* @param {string} [options.schemaDescription] - Description for the schema.
|
|
409
|
+
* @param {string} [options.prompt] - Original prompt for retry context.
|
|
410
|
+
* @param {Array} [options.messages] - Original messages for retry context.
|
|
411
|
+
* @returns {Promise<Object>} Generation result with validated object.
|
|
412
|
+
*/
|
|
413
|
+
const generateWithSchemaValidation = async ({
|
|
414
|
+
generationOptions,
|
|
415
|
+
schema,
|
|
416
|
+
schemaName,
|
|
417
|
+
schemaDescription,
|
|
418
|
+
prompt,
|
|
419
|
+
messages,
|
|
420
|
+
}) => {
|
|
421
|
+
let lastError = null;
|
|
422
|
+
let lastObject = null;
|
|
423
|
+
let wrappedSchema = false;
|
|
424
|
+
|
|
425
|
+
// If JSON schema with allOf/anyOf/oneOf at the top level, wrap it in an object
|
|
426
|
+
if (!isZodSchema(schema) && (schema.allOf || schema.anyOf || schema.oneOf)) {
|
|
427
|
+
schema = {
|
|
428
|
+
type: "object",
|
|
429
|
+
properties: {
|
|
430
|
+
object: schema,
|
|
431
|
+
},
|
|
432
|
+
required: ["object"],
|
|
433
|
+
additionalProperties: false,
|
|
434
|
+
};
|
|
435
|
+
wrappedSchema = true;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Convert schema to AI SDK format (wraps JSON schemas with jsonSchema())
|
|
439
|
+
const aiSdkSchema = toAiSdkSchema(schema);
|
|
440
|
+
|
|
441
|
+
for (let attempt = 1; attempt <= MAX_SCHEMA_VALIDATION_RETRIES; attempt++) {
|
|
442
|
+
const objectOptions = {
|
|
443
|
+
...generationOptions,
|
|
444
|
+
schema: aiSdkSchema,
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
if (schemaName) {
|
|
448
|
+
objectOptions.schemaName = schemaName;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (schemaDescription) {
|
|
452
|
+
objectOptions.schemaDescription = schemaDescription;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Add retry context if this is a retry attempt
|
|
456
|
+
if (attempt > 1 && lastError) {
|
|
457
|
+
const retryMessage = `Previous attempt failed schema validation with errors: ${lastError}. Please fix these issues and try again.`;
|
|
458
|
+
|
|
459
|
+
if (objectOptions.messages) {
|
|
460
|
+
// Add retry context to messages
|
|
461
|
+
objectOptions.messages = [
|
|
462
|
+
...objectOptions.messages,
|
|
463
|
+
{ role: "assistant", content: JSON.stringify(lastObject) },
|
|
464
|
+
{ role: "user", content: retryMessage },
|
|
465
|
+
];
|
|
466
|
+
} else if (typeof objectOptions.prompt === "string") {
|
|
467
|
+
// Add retry context to prompt
|
|
468
|
+
objectOptions.prompt = `${objectOptions.prompt}\n\n${retryMessage}`;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
try {
|
|
473
|
+
const result = await generateObject(objectOptions);
|
|
474
|
+
|
|
475
|
+
const validationObject = wrappedSchema
|
|
476
|
+
? result.object.object
|
|
477
|
+
: result.object;
|
|
478
|
+
const validationSchema = wrappedSchema
|
|
479
|
+
? schema.properties.object
|
|
480
|
+
: schema;
|
|
481
|
+
|
|
482
|
+
// Validate the generated object against the schema ourselves
|
|
483
|
+
const validation = validateAgainstSchema(
|
|
484
|
+
validationObject,
|
|
485
|
+
validationSchema
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
if (validation.valid) {
|
|
489
|
+
return {
|
|
490
|
+
object: validationObject,
|
|
491
|
+
usage: result.usage,
|
|
492
|
+
finishReason: result.finishReason,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Schema validation failed, store error for retry
|
|
497
|
+
lastError = validation.errors;
|
|
498
|
+
lastObject = result.object;
|
|
499
|
+
|
|
500
|
+
if (attempt === MAX_SCHEMA_VALIDATION_RETRIES) {
|
|
501
|
+
throw new Error(
|
|
502
|
+
`Schema validation failed after ${MAX_SCHEMA_VALIDATION_RETRIES} attempts. Last errors: ${validation.errors}`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
} catch (error) {
|
|
506
|
+
// If it's our validation error and we have retries left, continue
|
|
507
|
+
if (
|
|
508
|
+
error.message.includes("Schema validation failed after") ||
|
|
509
|
+
attempt === MAX_SCHEMA_VALIDATION_RETRIES
|
|
510
|
+
) {
|
|
511
|
+
throw error;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Store the error and retry
|
|
515
|
+
lastError = error.message;
|
|
516
|
+
lastObject = null;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
throw new Error(
|
|
521
|
+
`Schema validation failed after ${MAX_SCHEMA_VALIDATION_RETRIES} attempts. Last errors: ${lastError}`
|
|
522
|
+
);
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
module.exports = {
|
|
526
|
+
generate,
|
|
527
|
+
detectProvider,
|
|
528
|
+
getApiKey,
|
|
529
|
+
modelMap,
|
|
530
|
+
DEFAULT_MODEL,
|
|
531
|
+
MAX_SCHEMA_VALIDATION_RETRIES,
|
|
532
|
+
};
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,8 @@ const { schemas } = require("./schemas");
|
|
|
2
2
|
const { validate, transformToSchemaKey } = require("./validate");
|
|
3
3
|
const { resolvePaths } = require("./resolvePaths");
|
|
4
4
|
const { readFile } = require("./files");
|
|
5
|
+
const { generate, detectProvider, getApiKey, DEFAULT_MODEL, MAX_SCHEMA_VALIDATION_RETRIES } = require("./ai");
|
|
6
|
+
const { refineStep } = require("./refineStep");
|
|
5
7
|
|
|
6
8
|
module.exports = {
|
|
7
9
|
schemas,
|
|
@@ -9,4 +11,10 @@ module.exports = {
|
|
|
9
11
|
resolvePaths,
|
|
10
12
|
readFile,
|
|
11
13
|
transformToSchemaKey,
|
|
14
|
+
generate,
|
|
15
|
+
detectProvider,
|
|
16
|
+
getApiKey,
|
|
17
|
+
DEFAULT_MODEL,
|
|
18
|
+
MAX_SCHEMA_VALIDATION_RETRIES,
|
|
19
|
+
refineStep,
|
|
12
20
|
};
|