@probelabs/probe 0.6.0-rc149 → 0.6.0-rc151
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/build/agent/ProbeAgent.js +2 -2
- package/build/agent/index.js +6928 -246
- package/build/agent/schemaUtils.js +211 -3
- package/cjs/agent/ProbeAgent.cjs +7222 -540
- package/cjs/index.cjs +7222 -540
- package/package.json +4 -3
- package/src/agent/ProbeAgent.js +2 -2
- package/src/agent/schemaUtils.js +211 -3
|
@@ -5,6 +5,65 @@
|
|
|
5
5
|
|
|
6
6
|
import { createMessagePreview } from '../tools/common.js';
|
|
7
7
|
import { validate, fixText, extractMermaidBlocks } from '@probelabs/maid';
|
|
8
|
+
import Ajv from 'ajv';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Recursively apply additionalProperties: false to all object schemas
|
|
12
|
+
* This ensures strict validation at all nesting levels
|
|
13
|
+
* @param {Object} schema - JSON schema object
|
|
14
|
+
* @returns {Object} - Modified schema with additionalProperties enforced
|
|
15
|
+
*/
|
|
16
|
+
function enforceNoAdditionalProperties(schema) {
|
|
17
|
+
if (!schema || typeof schema !== 'object') {
|
|
18
|
+
return schema;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Create a deep clone to avoid modifying the original
|
|
22
|
+
const cloned = JSON.parse(JSON.stringify(schema));
|
|
23
|
+
|
|
24
|
+
function applyRecursively(obj) {
|
|
25
|
+
if (!obj || typeof obj !== 'object') {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// If this is an object type schema and doesn't have additionalProperties set
|
|
30
|
+
if (obj.type === 'object' && obj.additionalProperties === undefined) {
|
|
31
|
+
obj.additionalProperties = false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Recursively process properties
|
|
35
|
+
if (obj.properties && typeof obj.properties === 'object') {
|
|
36
|
+
Object.values(obj.properties).forEach(applyRecursively);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Process items for arrays
|
|
40
|
+
if (obj.items) {
|
|
41
|
+
if (Array.isArray(obj.items)) {
|
|
42
|
+
obj.items.forEach(applyRecursively);
|
|
43
|
+
} else {
|
|
44
|
+
applyRecursively(obj.items);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Process nested schemas in oneOf, anyOf, allOf
|
|
49
|
+
['oneOf', 'anyOf', 'allOf'].forEach(key => {
|
|
50
|
+
if (Array.isArray(obj[key])) {
|
|
51
|
+
obj[key].forEach(applyRecursively);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Process definitions/defs
|
|
56
|
+
if (obj.definitions && typeof obj.definitions === 'object') {
|
|
57
|
+
Object.values(obj.definitions).forEach(applyRecursively);
|
|
58
|
+
}
|
|
59
|
+
if (obj.$defs && typeof obj.$defs === 'object') {
|
|
60
|
+
Object.values(obj.$defs).forEach(applyRecursively);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
applyRecursively(cloned);
|
|
65
|
+
return cloned;
|
|
66
|
+
}
|
|
8
67
|
|
|
9
68
|
/**
|
|
10
69
|
* HTML entity decoder map for common entities that might appear in mermaid diagrams
|
|
@@ -141,12 +200,15 @@ export function cleanSchemaResponse(response) {
|
|
|
141
200
|
* @returns {Object} - {isValid: boolean, parsed?: Object, error?: string}
|
|
142
201
|
*/
|
|
143
202
|
export function validateJsonResponse(response, options = {}) {
|
|
144
|
-
const { debug = false } = options;
|
|
203
|
+
const { debug = false, schema = null, strictSchema = true } = options;
|
|
145
204
|
|
|
146
205
|
if (debug) {
|
|
147
206
|
console.log(`[DEBUG] JSON validation: Starting validation for response (${response.length} chars)`);
|
|
148
207
|
const preview = createMessagePreview(response);
|
|
149
208
|
console.log(`[DEBUG] JSON validation: Preview: ${preview}`);
|
|
209
|
+
if (schema) {
|
|
210
|
+
console.log(`[DEBUG] JSON validation: Schema validation enabled`);
|
|
211
|
+
}
|
|
150
212
|
}
|
|
151
213
|
|
|
152
214
|
try {
|
|
@@ -159,6 +221,140 @@ export function validateJsonResponse(response, options = {}) {
|
|
|
159
221
|
console.log(`[DEBUG] JSON validation: Object type: ${typeof parsed}, keys: ${Object.keys(parsed || {}).length}`);
|
|
160
222
|
}
|
|
161
223
|
|
|
224
|
+
// If schema provided, validate against it
|
|
225
|
+
if (schema) {
|
|
226
|
+
const schemaStart = Date.now();
|
|
227
|
+
let schemaObj;
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
// Parse schema if it's a string
|
|
231
|
+
schemaObj = typeof schema === 'string' ? JSON.parse(schema) : schema;
|
|
232
|
+
|
|
233
|
+
// Apply strict mode: enforce additionalProperties: false on all nested objects
|
|
234
|
+
// unless explicitly disabled via strictSchema: false option
|
|
235
|
+
if (strictSchema) {
|
|
236
|
+
schemaObj = enforceNoAdditionalProperties(schemaObj);
|
|
237
|
+
if (debug) {
|
|
238
|
+
console.log(`[DEBUG] JSON validation: Applied strict mode - additionalProperties: false enforced on all levels`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} catch (schemaParseError) {
|
|
242
|
+
if (debug) {
|
|
243
|
+
console.log(`[DEBUG] JSON validation: Failed to parse schema: ${schemaParseError.message}`);
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
isValid: false,
|
|
247
|
+
error: 'Invalid schema provided',
|
|
248
|
+
schemaError: schemaParseError.message
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Create AJV validator with strict mode
|
|
253
|
+
const ajv = new Ajv({
|
|
254
|
+
strict: false, // Don't fail on unknown schema keywords
|
|
255
|
+
allErrors: true, // Return all errors, not just the first
|
|
256
|
+
verbose: true // Include schema and data in errors
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
let validate;
|
|
260
|
+
try {
|
|
261
|
+
validate = ajv.compile(schemaObj);
|
|
262
|
+
} catch (compileError) {
|
|
263
|
+
if (debug) {
|
|
264
|
+
console.log(`[DEBUG] JSON validation: Schema compilation failed: ${compileError.message}`);
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
isValid: false,
|
|
268
|
+
error: 'Schema compilation failed',
|
|
269
|
+
schemaError: compileError.message
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const valid = validate(parsed);
|
|
274
|
+
const schemaTime = Date.now() - schemaStart;
|
|
275
|
+
|
|
276
|
+
if (debug) {
|
|
277
|
+
console.log(`[DEBUG] JSON validation: Schema validation completed in ${schemaTime}ms, valid: ${valid}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!valid) {
|
|
281
|
+
// Format schema errors for better readability and actionability
|
|
282
|
+
const formattedErrors = validate.errors.map(err => {
|
|
283
|
+
// Convert JSON Pointer path to dot notation for readability
|
|
284
|
+
// /user/profile/age -> user.profile.age
|
|
285
|
+
const path = err.instancePath
|
|
286
|
+
? err.instancePath.substring(1).replace(/\//g, '.')
|
|
287
|
+
: '<root>';
|
|
288
|
+
|
|
289
|
+
let message = '';
|
|
290
|
+
let suggestion = '';
|
|
291
|
+
|
|
292
|
+
// Create crisp, actionable error messages based on error type
|
|
293
|
+
if (err.keyword === 'additionalProperties') {
|
|
294
|
+
const extraField = err.params.additionalProperty;
|
|
295
|
+
message = `Extra field '${extraField}' is not allowed`;
|
|
296
|
+
suggestion = `Remove '${extraField}' or add it to the schema`;
|
|
297
|
+
} else if (err.keyword === 'required') {
|
|
298
|
+
const missingField = err.params.missingProperty;
|
|
299
|
+
message = `Missing required field '${missingField}'`;
|
|
300
|
+
suggestion = `Add '${missingField}' to this object`;
|
|
301
|
+
} else if (err.keyword === 'type') {
|
|
302
|
+
const expected = err.params.type;
|
|
303
|
+
const actual = typeof err.data;
|
|
304
|
+
const value = JSON.stringify(err.data);
|
|
305
|
+
message = `Wrong type: expected ${expected}, got ${actual} (value: ${value})`;
|
|
306
|
+
suggestion = `Change value to ${expected} type`;
|
|
307
|
+
} else if (err.keyword === 'enum') {
|
|
308
|
+
const allowed = err.params.allowedValues;
|
|
309
|
+
const value = JSON.stringify(err.data);
|
|
310
|
+
message = `Invalid value ${value}. Allowed: ${allowed.map(v => JSON.stringify(v)).join(', ')}`;
|
|
311
|
+
suggestion = `Use one of the allowed values`;
|
|
312
|
+
} else if (err.keyword === 'minimum' || err.keyword === 'maximum') {
|
|
313
|
+
const limit = err.params.limit;
|
|
314
|
+
const comparison = err.params.comparison;
|
|
315
|
+
message = `Value ${err.data} ${err.message} (${comparison} ${limit})`;
|
|
316
|
+
suggestion = `Adjust value to meet constraint`;
|
|
317
|
+
} else if (err.keyword === 'minLength' || err.keyword === 'maxLength') {
|
|
318
|
+
const limit = err.params.limit;
|
|
319
|
+
message = `String length ${err.message} (current: ${err.data.length}, ${err.keyword}: ${limit})`;
|
|
320
|
+
suggestion = `Adjust string length`;
|
|
321
|
+
} else if (err.keyword === 'pattern') {
|
|
322
|
+
message = `Value doesn't match required pattern: ${err.params.pattern}`;
|
|
323
|
+
suggestion = `Update value to match the pattern`;
|
|
324
|
+
} else {
|
|
325
|
+
// Fallback for other error types
|
|
326
|
+
message = err.message;
|
|
327
|
+
suggestion = '';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Format: "path: message | suggestion"
|
|
331
|
+
const location = path ? `at '${path}'` : 'at root';
|
|
332
|
+
return suggestion
|
|
333
|
+
? `${location}: ${message} → ${suggestion}`
|
|
334
|
+
: `${location}: ${message}`;
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const errorSummary = formattedErrors.join('\n ');
|
|
338
|
+
|
|
339
|
+
if (debug) {
|
|
340
|
+
console.log(`[DEBUG] JSON validation: Schema validation errors:\n ${errorSummary}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
isValid: false,
|
|
345
|
+
error: 'Schema validation failed',
|
|
346
|
+
schemaErrors: validate.errors,
|
|
347
|
+
formattedErrors: formattedErrors,
|
|
348
|
+
errorSummary: `Schema validation failed:\n ${errorSummary}`,
|
|
349
|
+
parsed: parsed // Still return parsed data for debugging
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (debug) {
|
|
354
|
+
console.log(`[DEBUG] JSON validation: Schema validation passed`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
162
358
|
return { isValid: true, parsed };
|
|
163
359
|
} catch (error) {
|
|
164
360
|
// Extract error position from error message if available
|
|
@@ -886,9 +1082,21 @@ When presented with broken JSON, analyze it thoroughly and provide the corrected
|
|
|
886
1082
|
errorContext = validationResult.enhancedError;
|
|
887
1083
|
}
|
|
888
1084
|
|
|
1085
|
+
// Add schema validation errors if present
|
|
1086
|
+
let schemaErrorDetails = '';
|
|
1087
|
+
if (validationResult.errorSummary) {
|
|
1088
|
+
schemaErrorDetails = `\n\nSchema Validation Errors:\n${validationResult.errorSummary}`;
|
|
1089
|
+
} else if (validationResult.schemaErrors && validationResult.schemaErrors.length > 0) {
|
|
1090
|
+
const errors = validationResult.schemaErrors.map(err => {
|
|
1091
|
+
const path = err.instancePath || '(root)';
|
|
1092
|
+
return ` ${path}: ${err.message}`;
|
|
1093
|
+
}).join('\n');
|
|
1094
|
+
schemaErrorDetails = `\n\nSchema Validation Errors:\n${errors}`;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
889
1097
|
const prompt = `Fix the following invalid JSON.
|
|
890
1098
|
|
|
891
|
-
Error: ${errorContext}
|
|
1099
|
+
Error: ${errorContext}${schemaErrorDetails}
|
|
892
1100
|
|
|
893
1101
|
Invalid JSON:
|
|
894
1102
|
${invalidJson}
|
|
@@ -896,7 +1104,7 @@ ${invalidJson}
|
|
|
896
1104
|
Expected schema structure:
|
|
897
1105
|
${schema}
|
|
898
1106
|
|
|
899
|
-
Provide only the corrected JSON without any markdown formatting or explanations.`;
|
|
1107
|
+
${schemaErrorDetails ? 'CRITICAL: Pay special attention to the schema validation errors above. The JSON may be syntactically valid but does not conform to the required schema. Make sure to:\n- Include all required fields\n- Use correct data types\n- Remove any additional properties not defined in the schema (if additionalProperties is false)\n- Ensure all values match their schema constraints\n\n' : ''}Provide only the corrected JSON without any markdown formatting or explanations.`;
|
|
900
1108
|
|
|
901
1109
|
try {
|
|
902
1110
|
if (this.options.debug) {
|