agentic-json-repair 1.0.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/.vscode/extensions.json +3 -0
- package/dist/index.d.mts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +115 -0
- package/dist/index.mjs +90 -0
- package/package.json +40 -0
- package/src/index.test.ts +35 -0
- package/src/index.ts +128 -0
- package/tsconfig.json +14 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
type AgenticParseResult<T> = {
|
|
4
|
+
success: true;
|
|
5
|
+
data: T;
|
|
6
|
+
} | {
|
|
7
|
+
success: false;
|
|
8
|
+
error: string;
|
|
9
|
+
retryPrompt: string;
|
|
10
|
+
};
|
|
11
|
+
declare function safeParseAI<T>(aiOutput: string, schema: z.ZodType<T>): AgenticParseResult<T>;
|
|
12
|
+
|
|
13
|
+
export { type AgenticParseResult, safeParseAI };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
type AgenticParseResult<T> = {
|
|
4
|
+
success: true;
|
|
5
|
+
data: T;
|
|
6
|
+
} | {
|
|
7
|
+
success: false;
|
|
8
|
+
error: string;
|
|
9
|
+
retryPrompt: string;
|
|
10
|
+
};
|
|
11
|
+
declare function safeParseAI<T>(aiOutput: string, schema: z.ZodType<T>): AgenticParseResult<T>;
|
|
12
|
+
|
|
13
|
+
export { type AgenticParseResult, safeParseAI };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
safeParseAI: () => safeParseAI
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
var import_jsonrepair = require("jsonrepair");
|
|
27
|
+
function extractJsonContent(aiOutput) {
|
|
28
|
+
const codeBlockMatch = aiOutput.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
29
|
+
if (codeBlockMatch) {
|
|
30
|
+
return codeBlockMatch[1].trim();
|
|
31
|
+
}
|
|
32
|
+
const firstBrace = aiOutput.indexOf("{");
|
|
33
|
+
const firstBracket = aiOutput.indexOf("[");
|
|
34
|
+
let startChar;
|
|
35
|
+
let endChar;
|
|
36
|
+
let startIndex;
|
|
37
|
+
if (firstBrace === -1 && firstBracket === -1) {
|
|
38
|
+
return aiOutput.trim();
|
|
39
|
+
} else if (firstBrace === -1) {
|
|
40
|
+
startChar = "[";
|
|
41
|
+
endChar = "]";
|
|
42
|
+
startIndex = firstBracket;
|
|
43
|
+
} else if (firstBracket === -1) {
|
|
44
|
+
startChar = "{";
|
|
45
|
+
endChar = "}";
|
|
46
|
+
startIndex = firstBrace;
|
|
47
|
+
} else {
|
|
48
|
+
if (firstBrace < firstBracket) {
|
|
49
|
+
startChar = "{";
|
|
50
|
+
endChar = "}";
|
|
51
|
+
startIndex = firstBrace;
|
|
52
|
+
} else {
|
|
53
|
+
startChar = "[";
|
|
54
|
+
endChar = "]";
|
|
55
|
+
startIndex = firstBracket;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
let depth = 0;
|
|
59
|
+
let inString = false;
|
|
60
|
+
let escapeNext = false;
|
|
61
|
+
for (let i = startIndex; i < aiOutput.length; i++) {
|
|
62
|
+
const char = aiOutput[i];
|
|
63
|
+
if (escapeNext) {
|
|
64
|
+
escapeNext = false;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (char === "\\" && inString) {
|
|
68
|
+
escapeNext = true;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (char === '"') {
|
|
72
|
+
inString = !inString;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (!inString) {
|
|
76
|
+
if (char === startChar) {
|
|
77
|
+
depth++;
|
|
78
|
+
} else if (char === endChar) {
|
|
79
|
+
depth--;
|
|
80
|
+
if (depth === 0) {
|
|
81
|
+
return aiOutput.slice(startIndex, i + 1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return aiOutput.slice(startIndex).trim();
|
|
87
|
+
}
|
|
88
|
+
function safeParseAI(aiOutput, schema) {
|
|
89
|
+
try {
|
|
90
|
+
const cleanOutput = extractJsonContent(aiOutput);
|
|
91
|
+
const repairedJson = (0, import_jsonrepair.jsonrepair)(cleanOutput);
|
|
92
|
+
const parsedObject = JSON.parse(repairedJson);
|
|
93
|
+
const validation = schema.safeParse(parsedObject);
|
|
94
|
+
if (validation.success) {
|
|
95
|
+
return { success: true, data: validation.data };
|
|
96
|
+
} else {
|
|
97
|
+
const issues = validation.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
|
|
98
|
+
return {
|
|
99
|
+
success: false,
|
|
100
|
+
error: "Validation Failed",
|
|
101
|
+
retryPrompt: `The JSON was parsed but failed validation. Fix these issues: [${issues}] and return ONLY the JSON.`
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return {
|
|
106
|
+
success: false,
|
|
107
|
+
error: "JSON Syntax Error",
|
|
108
|
+
retryPrompt: `The output was not valid JSON. Error: ${error.message}. Return ONLY valid JSON.`
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
113
|
+
0 && (module.exports = {
|
|
114
|
+
safeParseAI
|
|
115
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { jsonrepair } from "jsonrepair";
|
|
3
|
+
function extractJsonContent(aiOutput) {
|
|
4
|
+
const codeBlockMatch = aiOutput.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
5
|
+
if (codeBlockMatch) {
|
|
6
|
+
return codeBlockMatch[1].trim();
|
|
7
|
+
}
|
|
8
|
+
const firstBrace = aiOutput.indexOf("{");
|
|
9
|
+
const firstBracket = aiOutput.indexOf("[");
|
|
10
|
+
let startChar;
|
|
11
|
+
let endChar;
|
|
12
|
+
let startIndex;
|
|
13
|
+
if (firstBrace === -1 && firstBracket === -1) {
|
|
14
|
+
return aiOutput.trim();
|
|
15
|
+
} else if (firstBrace === -1) {
|
|
16
|
+
startChar = "[";
|
|
17
|
+
endChar = "]";
|
|
18
|
+
startIndex = firstBracket;
|
|
19
|
+
} else if (firstBracket === -1) {
|
|
20
|
+
startChar = "{";
|
|
21
|
+
endChar = "}";
|
|
22
|
+
startIndex = firstBrace;
|
|
23
|
+
} else {
|
|
24
|
+
if (firstBrace < firstBracket) {
|
|
25
|
+
startChar = "{";
|
|
26
|
+
endChar = "}";
|
|
27
|
+
startIndex = firstBrace;
|
|
28
|
+
} else {
|
|
29
|
+
startChar = "[";
|
|
30
|
+
endChar = "]";
|
|
31
|
+
startIndex = firstBracket;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
let depth = 0;
|
|
35
|
+
let inString = false;
|
|
36
|
+
let escapeNext = false;
|
|
37
|
+
for (let i = startIndex; i < aiOutput.length; i++) {
|
|
38
|
+
const char = aiOutput[i];
|
|
39
|
+
if (escapeNext) {
|
|
40
|
+
escapeNext = false;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (char === "\\" && inString) {
|
|
44
|
+
escapeNext = true;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (char === '"') {
|
|
48
|
+
inString = !inString;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (!inString) {
|
|
52
|
+
if (char === startChar) {
|
|
53
|
+
depth++;
|
|
54
|
+
} else if (char === endChar) {
|
|
55
|
+
depth--;
|
|
56
|
+
if (depth === 0) {
|
|
57
|
+
return aiOutput.slice(startIndex, i + 1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return aiOutput.slice(startIndex).trim();
|
|
63
|
+
}
|
|
64
|
+
function safeParseAI(aiOutput, schema) {
|
|
65
|
+
try {
|
|
66
|
+
const cleanOutput = extractJsonContent(aiOutput);
|
|
67
|
+
const repairedJson = jsonrepair(cleanOutput);
|
|
68
|
+
const parsedObject = JSON.parse(repairedJson);
|
|
69
|
+
const validation = schema.safeParse(parsedObject);
|
|
70
|
+
if (validation.success) {
|
|
71
|
+
return { success: true, data: validation.data };
|
|
72
|
+
} else {
|
|
73
|
+
const issues = validation.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
error: "Validation Failed",
|
|
77
|
+
retryPrompt: `The JSON was parsed but failed validation. Fix these issues: [${issues}] and return ONLY the JSON.`
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return {
|
|
82
|
+
success: false,
|
|
83
|
+
error: "JSON Syntax Error",
|
|
84
|
+
retryPrompt: `The output was not valid JSON. Error: ${error.message}. Return ONLY valid JSON.`
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export {
|
|
89
|
+
safeParseAI
|
|
90
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentic-json-repair",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Repair and validate AI-generated JSON with auto-generated retry prompts.",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"ai",
|
|
22
|
+
"llm",
|
|
23
|
+
"json",
|
|
24
|
+
"repair",
|
|
25
|
+
"zod",
|
|
26
|
+
"agent"
|
|
27
|
+
],
|
|
28
|
+
"author": "Your Name",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"jsonrepair": "^3.1.0",
|
|
32
|
+
"zod": "^3.22.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"tsup": "^8.0.0",
|
|
36
|
+
"typescript": "^5.0.0",
|
|
37
|
+
"vitest": "^1.0.0",
|
|
38
|
+
"@types/node": "^20.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { expect, test } from 'vitest';
|
|
2
|
+
import { safeParseAI } from './index';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
test('fixes broken AI output', () => {
|
|
6
|
+
const brokenAiOutput = `
|
|
7
|
+
Sure! Here is the user:
|
|
8
|
+
\`\`\`json
|
|
9
|
+
{
|
|
10
|
+
name: "Alice",
|
|
11
|
+
age: 30,
|
|
12
|
+
}
|
|
13
|
+
\`\`\`
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
const UserSchema = z.object({ name: z.string(), age: z.number() });
|
|
17
|
+
const result = safeParseAI(brokenAiOutput, UserSchema);
|
|
18
|
+
|
|
19
|
+
expect(result.success).toBe(true);
|
|
20
|
+
if (result.success) {
|
|
21
|
+
expect(result.data.name).toBe("Alice");
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('generates retry prompt on wrong schema', () => {
|
|
26
|
+
const wrongTypeOutput = `{"age": "thirty"}`; // string instead of number
|
|
27
|
+
const Schema = z.object({ age: z.number() });
|
|
28
|
+
|
|
29
|
+
const result = safeParseAI(wrongTypeOutput, Schema);
|
|
30
|
+
|
|
31
|
+
expect(result.success).toBe(false);
|
|
32
|
+
if (!result.success) {
|
|
33
|
+
expect(result.retryPrompt).toContain("expected number, received string");
|
|
34
|
+
}
|
|
35
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { jsonrepair } from 'jsonrepair';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
export type AgenticParseResult<T> =
|
|
5
|
+
| { success: true; data: T }
|
|
6
|
+
| { success: false; error: string; retryPrompt: string };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Attempts to extract, repair, and validate JSON from an AI's text output.
|
|
10
|
+
* * @param aiOutput The raw string from the LLM (e.g., "Here is the data: ```json ...")
|
|
11
|
+
* @param schema The Zod schema to validate against
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Extracts JSON content from AI output that may contain markdown code blocks or prose.
|
|
15
|
+
*/
|
|
16
|
+
function extractJsonContent(aiOutput: string): string {
|
|
17
|
+
// First, try to extract from markdown code blocks
|
|
18
|
+
const codeBlockMatch = aiOutput.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
19
|
+
if (codeBlockMatch) {
|
|
20
|
+
return codeBlockMatch[1].trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Otherwise, try to find the first JSON object or array
|
|
24
|
+
const firstBrace = aiOutput.indexOf('{');
|
|
25
|
+
const firstBracket = aiOutput.indexOf('[');
|
|
26
|
+
|
|
27
|
+
let startChar: '{' | '[';
|
|
28
|
+
let endChar: '}' | ']';
|
|
29
|
+
let startIndex: number;
|
|
30
|
+
|
|
31
|
+
if (firstBrace === -1 && firstBracket === -1) {
|
|
32
|
+
// No JSON structure found, return as-is for jsonrepair to handle
|
|
33
|
+
return aiOutput.trim();
|
|
34
|
+
} else if (firstBrace === -1) {
|
|
35
|
+
startChar = '[';
|
|
36
|
+
endChar = ']';
|
|
37
|
+
startIndex = firstBracket;
|
|
38
|
+
} else if (firstBracket === -1) {
|
|
39
|
+
startChar = '{';
|
|
40
|
+
endChar = '}';
|
|
41
|
+
startIndex = firstBrace;
|
|
42
|
+
} else {
|
|
43
|
+
// Both found, use whichever comes first
|
|
44
|
+
if (firstBrace < firstBracket) {
|
|
45
|
+
startChar = '{';
|
|
46
|
+
endChar = '}';
|
|
47
|
+
startIndex = firstBrace;
|
|
48
|
+
} else {
|
|
49
|
+
startChar = '[';
|
|
50
|
+
endChar = ']';
|
|
51
|
+
startIndex = firstBracket;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Find the matching closing bracket/brace
|
|
56
|
+
let depth = 0;
|
|
57
|
+
let inString = false;
|
|
58
|
+
let escapeNext = false;
|
|
59
|
+
|
|
60
|
+
for (let i = startIndex; i < aiOutput.length; i++) {
|
|
61
|
+
const char = aiOutput[i];
|
|
62
|
+
|
|
63
|
+
if (escapeNext) {
|
|
64
|
+
escapeNext = false;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (char === '\\' && inString) {
|
|
69
|
+
escapeNext = true;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (char === '"') {
|
|
74
|
+
inString = !inString;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!inString) {
|
|
79
|
+
if (char === startChar) {
|
|
80
|
+
depth++;
|
|
81
|
+
} else if (char === endChar) {
|
|
82
|
+
depth--;
|
|
83
|
+
if (depth === 0) {
|
|
84
|
+
return aiOutput.slice(startIndex, i + 1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// If we couldn't find matching brackets, return from startIndex to end
|
|
91
|
+
return aiOutput.slice(startIndex).trim();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function safeParseAI<T>(aiOutput: string, schema: z.ZodType<T>): AgenticParseResult<T> {
|
|
95
|
+
try {
|
|
96
|
+
// 1. Extract JSON: Find the JSON content within the AI output
|
|
97
|
+
const cleanOutput = extractJsonContent(aiOutput);
|
|
98
|
+
|
|
99
|
+
// 2. Repair JSON: Fixes missing quotes, trailing commas, etc.
|
|
100
|
+
const repairedJson = jsonrepair(cleanOutput);
|
|
101
|
+
|
|
102
|
+
// 3. Parse JSON
|
|
103
|
+
const parsedObject = JSON.parse(repairedJson);
|
|
104
|
+
|
|
105
|
+
// 4. Validate Schema
|
|
106
|
+
const validation = schema.safeParse(parsedObject);
|
|
107
|
+
|
|
108
|
+
if (validation.success) {
|
|
109
|
+
return { success: true, data: validation.data };
|
|
110
|
+
} else {
|
|
111
|
+
// 5. Generate a "Self-Correction" prompt for the AI
|
|
112
|
+
const issues = validation.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join(', ');
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
error: "Validation Failed",
|
|
116
|
+
retryPrompt: `The JSON was parsed but failed validation. Fix these issues: [${issues}] and return ONLY the JSON.`
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
} catch (error) {
|
|
121
|
+
// 6. Handle Syntax Errors that jsonrepair couldn't fix
|
|
122
|
+
return {
|
|
123
|
+
success: false,
|
|
124
|
+
error: "JSON Syntax Error",
|
|
125
|
+
retryPrompt: `The output was not valid JSON. Error: ${(error as Error).message}. Return ONLY valid JSON.`
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"outDir": "dist",
|
|
10
|
+
"skipLibCheck": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["src"],
|
|
13
|
+
"exclude": ["node_modules", "dist"]
|
|
14
|
+
}
|