@probelabs/probe 0.6.0-rc100
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/README.md +583 -0
- package/bin/.gitkeep +0 -0
- package/bin/probe +158 -0
- package/bin/probe-binary +0 -0
- package/build/agent/ProbeAgent.d.ts +199 -0
- package/build/agent/ProbeAgent.js +1486 -0
- package/build/agent/acp/README.md +347 -0
- package/build/agent/acp/connection.js +237 -0
- package/build/agent/acp/connection.test.js +311 -0
- package/build/agent/acp/examples/simple-client.js +212 -0
- package/build/agent/acp/examples/tool-lifecycle.js +230 -0
- package/build/agent/acp/final-test.js +173 -0
- package/build/agent/acp/index.js +5 -0
- package/build/agent/acp/integration.test.js +385 -0
- package/build/agent/acp/manual-test.js +410 -0
- package/build/agent/acp/protocol-test.js +190 -0
- package/build/agent/acp/server.js +448 -0
- package/build/agent/acp/server.test.js +371 -0
- package/build/agent/acp/test-runner.js +216 -0
- package/build/agent/acp/test-utils/README.md +315 -0
- package/build/agent/acp/test-utils/acp-tester.js +484 -0
- package/build/agent/acp/test-utils/mock-acp-client.js +434 -0
- package/build/agent/acp/tools.js +368 -0
- package/build/agent/acp/tools.test.js +334 -0
- package/build/agent/acp/types.js +218 -0
- package/build/agent/acp/types.test.js +327 -0
- package/build/agent/appTracer.js +360 -0
- package/build/agent/fileSpanExporter.js +169 -0
- package/build/agent/index.js +7426 -0
- package/build/agent/mcp/client.js +338 -0
- package/build/agent/mcp/config.js +313 -0
- package/build/agent/mcp/index.js +64 -0
- package/build/agent/mcp/xmlBridge.js +371 -0
- package/build/agent/mockProvider.js +53 -0
- package/build/agent/probeTool.js +257 -0
- package/build/agent/schemaUtils.js +1726 -0
- package/build/agent/simpleTelemetry.js +267 -0
- package/build/agent/telemetry.js +225 -0
- package/build/agent/tokenCounter.js +395 -0
- package/build/agent/tools.js +163 -0
- package/build/cli.js +49 -0
- package/build/delegate.js +267 -0
- package/build/directory-resolver.js +237 -0
- package/build/downloader.js +750 -0
- package/build/extract.js +149 -0
- package/build/index.js +70 -0
- package/build/mcp/index.js +514 -0
- package/build/mcp/index.ts +608 -0
- package/build/query.js +116 -0
- package/build/search.js +247 -0
- package/build/tools/common.js +410 -0
- package/build/tools/index.js +40 -0
- package/build/tools/langchain.js +88 -0
- package/build/tools/system-message.js +121 -0
- package/build/tools/vercel.js +271 -0
- package/build/utils/file-lister.js +193 -0
- package/build/utils.js +128 -0
- package/cjs/agent/ProbeAgent.cjs +5829 -0
- package/cjs/index.cjs +6217 -0
- package/cjs/package.json +3 -0
- package/index.d.ts +401 -0
- package/package.json +114 -0
- package/scripts/postinstall.js +172 -0
- package/src/agent/ProbeAgent.d.ts +199 -0
- package/src/agent/ProbeAgent.js +1486 -0
- package/src/agent/acp/README.md +347 -0
- package/src/agent/acp/connection.js +237 -0
- package/src/agent/acp/connection.test.js +311 -0
- package/src/agent/acp/examples/simple-client.js +212 -0
- package/src/agent/acp/examples/tool-lifecycle.js +230 -0
- package/src/agent/acp/final-test.js +173 -0
- package/src/agent/acp/index.js +5 -0
- package/src/agent/acp/integration.test.js +385 -0
- package/src/agent/acp/manual-test.js +410 -0
- package/src/agent/acp/protocol-test.js +190 -0
- package/src/agent/acp/server.js +448 -0
- package/src/agent/acp/server.test.js +371 -0
- package/src/agent/acp/test-runner.js +216 -0
- package/src/agent/acp/test-utils/README.md +315 -0
- package/src/agent/acp/test-utils/acp-tester.js +484 -0
- package/src/agent/acp/test-utils/mock-acp-client.js +434 -0
- package/src/agent/acp/tools.js +368 -0
- package/src/agent/acp/tools.test.js +334 -0
- package/src/agent/acp/types.js +218 -0
- package/src/agent/acp/types.test.js +327 -0
- package/src/agent/appTracer.js +360 -0
- package/src/agent/fileSpanExporter.js +169 -0
- package/src/agent/index.js +813 -0
- package/src/agent/mcp/client.js +338 -0
- package/src/agent/mcp/config.js +313 -0
- package/src/agent/mcp/index.js +64 -0
- package/src/agent/mcp/xmlBridge.js +371 -0
- package/src/agent/mockProvider.js +53 -0
- package/src/agent/probeTool.js +257 -0
- package/src/agent/schemaUtils.js +1726 -0
- package/src/agent/simpleTelemetry.js +267 -0
- package/src/agent/telemetry.js +225 -0
- package/src/agent/tokenCounter.js +395 -0
- package/src/agent/tools.js +163 -0
- package/src/cli.js +49 -0
- package/src/delegate.js +267 -0
- package/src/directory-resolver.js +237 -0
- package/src/downloader.js +750 -0
- package/src/extract.js +149 -0
- package/src/index.js +70 -0
- package/src/mcp/index.ts +608 -0
- package/src/query.js +116 -0
- package/src/search.js +247 -0
- package/src/tools/common.js +410 -0
- package/src/tools/index.js +40 -0
- package/src/tools/langchain.js +88 -0
- package/src/tools/system-message.js +121 -0
- package/src/tools/vercel.js +271 -0
- package/src/utils/file-lister.js +193 -0
- package/src/utils.js +128 -0
|
@@ -0,0 +1,1726 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for cleaning and validating schema responses from AI models
|
|
3
|
+
* Supports JSON and Mermaid diagram validation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createMessagePreview } from '../tools/common.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* HTML entity decoder map for common entities that might appear in mermaid diagrams
|
|
10
|
+
*/
|
|
11
|
+
const HTML_ENTITY_MAP = {
|
|
12
|
+
'<': '<',
|
|
13
|
+
'>': '>',
|
|
14
|
+
'&': '&',
|
|
15
|
+
'"': '"',
|
|
16
|
+
''': "'",
|
|
17
|
+
' ': ' '
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Decode HTML entities in text without requiring external dependencies
|
|
22
|
+
* @param {string} text - Text that may contain HTML entities
|
|
23
|
+
* @returns {string} - Text with HTML entities decoded
|
|
24
|
+
*/
|
|
25
|
+
export function decodeHtmlEntities(text) {
|
|
26
|
+
if (!text || typeof text !== 'string') {
|
|
27
|
+
return text;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let decoded = text;
|
|
31
|
+
for (const [entity, character] of Object.entries(HTML_ENTITY_MAP)) {
|
|
32
|
+
// Use global replacement to catch all instances
|
|
33
|
+
decoded = decoded.replace(new RegExp(entity, 'g'), character);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return decoded;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Clean AI response by extracting JSON content when response contains JSON
|
|
41
|
+
* Only processes responses that contain JSON structures { or [
|
|
42
|
+
* @param {string} response - Raw AI response
|
|
43
|
+
* @returns {string} - Cleaned response with JSON boundaries extracted if applicable
|
|
44
|
+
*/
|
|
45
|
+
export function cleanSchemaResponse(response) {
|
|
46
|
+
if (!response || typeof response !== 'string') {
|
|
47
|
+
return response;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const trimmed = response.trim();
|
|
51
|
+
|
|
52
|
+
// First, look for JSON after code block markers
|
|
53
|
+
const codeBlockPatterns = [
|
|
54
|
+
/```json\s*\n?([{\[][\s\S]*?[}\]])\s*\n?```/,
|
|
55
|
+
/```\s*\n?([{\[][\s\S]*?[}\]])\s*\n?```/,
|
|
56
|
+
/`([{\[][\s\S]*?[}\]])`/
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
for (const pattern of codeBlockPatterns) {
|
|
60
|
+
const match = trimmed.match(pattern);
|
|
61
|
+
if (match) {
|
|
62
|
+
return match[1].trim();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Look for code block start followed immediately by JSON
|
|
67
|
+
const codeBlockStartPattern = /```(?:json)?\s*\n?\s*([{\[])/;
|
|
68
|
+
const codeBlockMatch = trimmed.match(codeBlockStartPattern);
|
|
69
|
+
|
|
70
|
+
if (codeBlockMatch) {
|
|
71
|
+
const startIndex = codeBlockMatch.index + codeBlockMatch[0].length - 1; // Position of the bracket
|
|
72
|
+
|
|
73
|
+
// Find the matching closing bracket
|
|
74
|
+
const openChar = codeBlockMatch[1];
|
|
75
|
+
const closeChar = openChar === '{' ? '}' : ']';
|
|
76
|
+
let bracketCount = 1;
|
|
77
|
+
let endIndex = startIndex + 1;
|
|
78
|
+
|
|
79
|
+
while (endIndex < trimmed.length && bracketCount > 0) {
|
|
80
|
+
const char = trimmed[endIndex];
|
|
81
|
+
if (char === openChar) {
|
|
82
|
+
bracketCount++;
|
|
83
|
+
} else if (char === closeChar) {
|
|
84
|
+
bracketCount--;
|
|
85
|
+
}
|
|
86
|
+
endIndex++;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (bracketCount === 0) {
|
|
90
|
+
return trimmed.substring(startIndex, endIndex);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Fallback: Find JSON boundaries anywhere in the text
|
|
95
|
+
const firstBracket = Math.min(
|
|
96
|
+
trimmed.indexOf('{') >= 0 ? trimmed.indexOf('{') : Infinity,
|
|
97
|
+
trimmed.indexOf('[') >= 0 ? trimmed.indexOf('[') : Infinity
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const lastBracket = Math.max(
|
|
101
|
+
trimmed.lastIndexOf('}'),
|
|
102
|
+
trimmed.lastIndexOf(']')
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Only extract if we found valid JSON boundaries
|
|
106
|
+
if (firstBracket < Infinity && lastBracket >= 0 && firstBracket < lastBracket) {
|
|
107
|
+
// Check if the response likely starts with JSON (directly or after minimal content)
|
|
108
|
+
const beforeFirstBracket = trimmed.substring(0, firstBracket).trim();
|
|
109
|
+
|
|
110
|
+
// If there's minimal content before the first bracket, extract the JSON
|
|
111
|
+
if (beforeFirstBracket === '' ||
|
|
112
|
+
beforeFirstBracket.match(/^```\w*$/) ||
|
|
113
|
+
beforeFirstBracket.split('\n').length <= 2) {
|
|
114
|
+
return trimmed.substring(firstBracket, lastBracket + 1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return response; // Return original if no extractable JSON found
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Validate that the cleaned response is valid JSON if expected
|
|
123
|
+
* @param {string} response - Cleaned response
|
|
124
|
+
* @param {Object} options - Options for validation
|
|
125
|
+
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
126
|
+
* @returns {Object} - {isValid: boolean, parsed?: Object, error?: string}
|
|
127
|
+
*/
|
|
128
|
+
export function validateJsonResponse(response, options = {}) {
|
|
129
|
+
const { debug = false } = options;
|
|
130
|
+
|
|
131
|
+
if (debug) {
|
|
132
|
+
console.log(`[DEBUG] JSON validation: Starting validation for response (${response.length} chars)`);
|
|
133
|
+
const preview = createMessagePreview(response);
|
|
134
|
+
console.log(`[DEBUG] JSON validation: Preview: ${preview}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const parseStart = Date.now();
|
|
139
|
+
const parsed = JSON.parse(response);
|
|
140
|
+
const parseTime = Date.now() - parseStart;
|
|
141
|
+
|
|
142
|
+
if (debug) {
|
|
143
|
+
console.log(`[DEBUG] JSON validation: Successfully parsed in ${parseTime}ms`);
|
|
144
|
+
console.log(`[DEBUG] JSON validation: Object type: ${typeof parsed}, keys: ${Object.keys(parsed || {}).length}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { isValid: true, parsed };
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (debug) {
|
|
150
|
+
console.log(`[DEBUG] JSON validation: Parse failed with error: ${error.message}`);
|
|
151
|
+
console.log(`[DEBUG] JSON validation: Error at position: ${error.message.match(/position (\d+)/) ? error.message.match(/position (\d+)/)[1] : 'unknown'}`);
|
|
152
|
+
|
|
153
|
+
// Try to identify common JSON issues
|
|
154
|
+
if (error.message.includes('Unexpected token')) {
|
|
155
|
+
console.log(`[DEBUG] JSON validation: Likely syntax error - unexpected character`);
|
|
156
|
+
} else if (error.message.includes('Unexpected end')) {
|
|
157
|
+
console.log(`[DEBUG] JSON validation: Likely incomplete JSON - missing closing brackets`);
|
|
158
|
+
} else if (error.message.includes('property name')) {
|
|
159
|
+
console.log(`[DEBUG] JSON validation: Likely unquoted property names`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { isValid: false, error: error.message };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Validate that the cleaned response is valid XML if expected
|
|
169
|
+
* @param {string} response - Cleaned response
|
|
170
|
+
* @returns {Object} - {isValid: boolean, error?: string}
|
|
171
|
+
*/
|
|
172
|
+
export function validateXmlResponse(response) {
|
|
173
|
+
// Basic XML validation - check for matching opening/closing tags
|
|
174
|
+
const xmlPattern = /<\/?[\w\s="'.-]+>/g;
|
|
175
|
+
const tags = response.match(xmlPattern);
|
|
176
|
+
|
|
177
|
+
if (!tags) {
|
|
178
|
+
return { isValid: false, error: 'No XML tags found' };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Simple check for basic XML structure
|
|
182
|
+
if (response.includes('<') && response.includes('>')) {
|
|
183
|
+
return { isValid: true };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { isValid: false, error: 'Invalid XML structure' };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Process schema response with cleaning and optional validation
|
|
191
|
+
* @param {string} response - Raw AI response
|
|
192
|
+
* @param {string} schema - Original schema for context
|
|
193
|
+
* @param {Object} options - Processing options
|
|
194
|
+
* @returns {Object} - {cleaned: string, validation?: Object}
|
|
195
|
+
*/
|
|
196
|
+
export function processSchemaResponse(response, schema, options = {}) {
|
|
197
|
+
const { validateJson = false, validateXml = false, debug = false } = options;
|
|
198
|
+
|
|
199
|
+
if (debug) {
|
|
200
|
+
console.log(`[DEBUG] Schema processing: Starting with response length ${response.length}`);
|
|
201
|
+
console.log(`[DEBUG] Schema processing: Schema type detection...`);
|
|
202
|
+
|
|
203
|
+
if (isJsonSchema(schema)) {
|
|
204
|
+
console.log(`[DEBUG] Schema processing: Detected JSON schema`);
|
|
205
|
+
} else {
|
|
206
|
+
console.log(`[DEBUG] Schema processing: Non-JSON schema detected`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Clean the response
|
|
211
|
+
const cleanStart = Date.now();
|
|
212
|
+
const cleaned = cleanSchemaResponse(response);
|
|
213
|
+
const cleanTime = Date.now() - cleanStart;
|
|
214
|
+
|
|
215
|
+
const result = { cleaned };
|
|
216
|
+
|
|
217
|
+
if (debug) {
|
|
218
|
+
console.log(`[DEBUG] Schema processing: Cleaning completed in ${cleanTime}ms`);
|
|
219
|
+
result.debug = {
|
|
220
|
+
originalLength: response.length,
|
|
221
|
+
cleanedLength: cleaned.length,
|
|
222
|
+
wasModified: response !== cleaned,
|
|
223
|
+
cleaningTimeMs: cleanTime,
|
|
224
|
+
removedContent: response !== cleaned ? {
|
|
225
|
+
before: response.substring(0, 100) + (response.length > 100 ? '...' : ''),
|
|
226
|
+
after: cleaned.substring(0, 100) + (cleaned.length > 100 ? '...' : '')
|
|
227
|
+
} : null
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
if (response !== cleaned) {
|
|
231
|
+
console.log(`[DEBUG] Schema processing: Response was modified during cleaning`);
|
|
232
|
+
console.log(`[DEBUG] Schema processing: Original length: ${response.length}, cleaned length: ${cleaned.length}`);
|
|
233
|
+
} else {
|
|
234
|
+
console.log(`[DEBUG] Schema processing: Response unchanged during cleaning`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Optional validation
|
|
239
|
+
if (validateJson) {
|
|
240
|
+
if (debug) {
|
|
241
|
+
console.log(`[DEBUG] Schema processing: Running JSON validation...`);
|
|
242
|
+
}
|
|
243
|
+
result.jsonValidation = validateJsonResponse(cleaned, { debug });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (validateXml) {
|
|
247
|
+
if (debug) {
|
|
248
|
+
console.log(`[DEBUG] Schema processing: Running XML validation...`);
|
|
249
|
+
}
|
|
250
|
+
result.xmlValidation = validateXmlResponse(cleaned);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Detect if a schema expects JSON output
|
|
258
|
+
* @param {string} schema - The schema string
|
|
259
|
+
* @returns {boolean} - True if schema appears to be JSON-based
|
|
260
|
+
*/
|
|
261
|
+
export function isJsonSchema(schema) {
|
|
262
|
+
if (!schema || typeof schema !== 'string') {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const trimmedSchema = schema.trim().toLowerCase();
|
|
267
|
+
|
|
268
|
+
// Check for JSON-like patterns
|
|
269
|
+
const jsonIndicators = [
|
|
270
|
+
trimmedSchema.startsWith('{') && trimmedSchema.includes('}'),
|
|
271
|
+
trimmedSchema.startsWith('[') && trimmedSchema.includes(']'),
|
|
272
|
+
trimmedSchema.includes('"type"') && trimmedSchema.includes('object'),
|
|
273
|
+
trimmedSchema.includes('"properties"'),
|
|
274
|
+
trimmedSchema.includes('json'),
|
|
275
|
+
trimmedSchema.includes('application/json')
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
// Return true if any JSON indicators are found
|
|
279
|
+
return jsonIndicators.some(indicator => indicator);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Detect if a JSON response is actually a JSON schema definition instead of data
|
|
284
|
+
* @param {string} jsonString - The JSON string to check
|
|
285
|
+
* @param {Object} options - Options
|
|
286
|
+
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
287
|
+
* @returns {boolean} - True if this appears to be a schema definition
|
|
288
|
+
*/
|
|
289
|
+
export function isJsonSchemaDefinition(jsonString, options = {}) {
|
|
290
|
+
const { debug = false } = options;
|
|
291
|
+
|
|
292
|
+
if (!jsonString || typeof jsonString !== 'string') {
|
|
293
|
+
if (debug) {
|
|
294
|
+
console.log(`[DEBUG] Schema definition check: Invalid input (${typeof jsonString})`);
|
|
295
|
+
}
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const parsed = JSON.parse(jsonString);
|
|
301
|
+
|
|
302
|
+
if (debug) {
|
|
303
|
+
console.log(`[DEBUG] Schema definition check: JSON parsed successfully, checking indicators...`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Check for common JSON schema properties
|
|
307
|
+
const schemaIndicators = [
|
|
308
|
+
parsed.$schema,
|
|
309
|
+
parsed.$id,
|
|
310
|
+
parsed.title && parsed.description,
|
|
311
|
+
parsed.type === 'object' && parsed.properties,
|
|
312
|
+
parsed.type === 'array' && parsed.items,
|
|
313
|
+
parsed.required && Array.isArray(parsed.required),
|
|
314
|
+
parsed.definitions,
|
|
315
|
+
parsed.additionalProperties !== undefined,
|
|
316
|
+
parsed.patternProperties,
|
|
317
|
+
parsed.anyOf || parsed.oneOf || parsed.allOf
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
const indicatorCount = schemaIndicators.filter(Boolean).length;
|
|
321
|
+
const isSchemaDefinition = indicatorCount >= 2;
|
|
322
|
+
|
|
323
|
+
if (debug) {
|
|
324
|
+
console.log(`[DEBUG] Schema definition check: Found ${indicatorCount} schema indicators`);
|
|
325
|
+
console.log(`[DEBUG] Schema definition check: Indicators found: ${schemaIndicators.map((indicator, i) => {
|
|
326
|
+
const names = ['$schema', '$id', 'title+description', 'object+properties', 'array+items', 'required', 'definitions', 'additionalProperties', 'patternProperties', 'anyOf/oneOf/allOf'];
|
|
327
|
+
return indicator ? names[i] : null;
|
|
328
|
+
}).filter(Boolean).join(', ')}`);
|
|
329
|
+
console.log(`[DEBUG] Schema definition check: Is schema definition: ${isSchemaDefinition}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return isSchemaDefinition;
|
|
333
|
+
} catch (error) {
|
|
334
|
+
if (debug) {
|
|
335
|
+
console.log(`[DEBUG] Schema definition check: JSON parse failed: ${error.message}`);
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Create a correction prompt for invalid JSON
|
|
343
|
+
* @param {string} invalidResponse - The invalid JSON response
|
|
344
|
+
* @param {string} schema - The original schema
|
|
345
|
+
* @param {string} error - The JSON parsing error
|
|
346
|
+
* @param {number} [retryCount=0] - The current retry attempt (0-based)
|
|
347
|
+
* @returns {string} - Correction prompt for the AI
|
|
348
|
+
*/
|
|
349
|
+
export function createJsonCorrectionPrompt(invalidResponse, schema, error, retryCount = 0) {
|
|
350
|
+
// Create increasingly stronger prompts based on retry attempt
|
|
351
|
+
const strengthLevels = [
|
|
352
|
+
{
|
|
353
|
+
prefix: "CRITICAL JSON ERROR:",
|
|
354
|
+
instruction: "You MUST fix this and return ONLY valid JSON.",
|
|
355
|
+
emphasis: "Return ONLY the corrected JSON, with no additional text or markdown formatting."
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
prefix: "URGENT - JSON PARSING FAILED:",
|
|
359
|
+
instruction: "This is your second chance. Return ONLY valid JSON that can be parsed by JSON.parse().",
|
|
360
|
+
emphasis: "ABSOLUTELY NO explanatory text, greetings, or formatting. ONLY JSON."
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
prefix: "FINAL ATTEMPT - CRITICAL JSON ERROR:",
|
|
364
|
+
instruction: "This is the final retry. You MUST return ONLY raw JSON without any other content.",
|
|
365
|
+
emphasis: "EXAMPLE: {\"key\": \"value\"} NOT: ```json{\"key\": \"value\"}``` NOT: Here is the JSON: {\"key\": \"value\"}"
|
|
366
|
+
}
|
|
367
|
+
];
|
|
368
|
+
|
|
369
|
+
const level = Math.min(retryCount, strengthLevels.length - 1);
|
|
370
|
+
const currentLevel = strengthLevels[level];
|
|
371
|
+
|
|
372
|
+
let prompt = `${currentLevel.prefix} Your previous response is not valid JSON and cannot be parsed. Here's what you returned:
|
|
373
|
+
|
|
374
|
+
${invalidResponse.substring(0, 500)}${invalidResponse.length > 500 ? '...' : ''}
|
|
375
|
+
|
|
376
|
+
Error: ${error}
|
|
377
|
+
|
|
378
|
+
${currentLevel.instruction}
|
|
379
|
+
|
|
380
|
+
Schema to match:
|
|
381
|
+
${schema}
|
|
382
|
+
|
|
383
|
+
${currentLevel.emphasis}`;
|
|
384
|
+
|
|
385
|
+
return prompt;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Create a correction prompt specifically for when AI returns schema definition instead of data
|
|
390
|
+
* @param {string} schemaDefinition - The JSON schema definition that was incorrectly returned
|
|
391
|
+
* @param {string} originalSchema - The original schema that should be followed
|
|
392
|
+
* @param {number} [retryCount=0] - The current retry attempt (0-based)
|
|
393
|
+
* @returns {string} - Correction prompt for the AI
|
|
394
|
+
*/
|
|
395
|
+
export function createSchemaDefinitionCorrectionPrompt(schemaDefinition, originalSchema, retryCount = 0) {
|
|
396
|
+
const strengthLevels = [
|
|
397
|
+
{
|
|
398
|
+
prefix: "CRITICAL MISUNDERSTANDING:",
|
|
399
|
+
instruction: "You returned a JSON schema definition instead of data. You must return ACTUAL DATA that follows the schema.",
|
|
400
|
+
example: "Instead of: {\"type\": \"object\", \"properties\": {...}}\nReturn: {\"actualData\": \"value\", \"realField\": 123}"
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
prefix: "URGENT - WRONG RESPONSE TYPE:",
|
|
404
|
+
instruction: "You are returning the SCHEMA DEFINITION itself. I need DATA that MATCHES the schema, not the schema structure.",
|
|
405
|
+
example: "Schema defines structure - you provide content that fits that structure!"
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
prefix: "FINAL ATTEMPT - SCHEMA VS DATA CONFUSION:",
|
|
409
|
+
instruction: "STOP returning schema definitions! Return REAL DATA that conforms to the schema structure.",
|
|
410
|
+
example: "If schema has 'properties.name', return {\"name\": \"actual_value\"} NOT {\"properties\": {\"name\": {...}}}"
|
|
411
|
+
}
|
|
412
|
+
];
|
|
413
|
+
|
|
414
|
+
const level = Math.min(retryCount, strengthLevels.length - 1);
|
|
415
|
+
const currentLevel = strengthLevels[level];
|
|
416
|
+
|
|
417
|
+
let prompt = `${currentLevel.prefix} You returned a JSON schema definition when I asked for data that matches a schema.
|
|
418
|
+
|
|
419
|
+
What you returned (WRONG - this is a schema definition):
|
|
420
|
+
${schemaDefinition.substring(0, 300)}${schemaDefinition.length > 300 ? '...' : ''}
|
|
421
|
+
|
|
422
|
+
What I need: ACTUAL DATA that conforms to this schema structure:
|
|
423
|
+
${originalSchema}
|
|
424
|
+
|
|
425
|
+
${currentLevel.instruction}
|
|
426
|
+
|
|
427
|
+
${currentLevel.example}
|
|
428
|
+
|
|
429
|
+
Return ONLY the JSON data object/array that follows the schema structure. NO schema definitions, NO explanations, NO markdown formatting.`;
|
|
430
|
+
|
|
431
|
+
return prompt;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Detect if a schema expects Mermaid diagram output
|
|
436
|
+
* @param {string} schema - The schema string
|
|
437
|
+
* @returns {boolean} - True if schema appears to expect Mermaid diagrams
|
|
438
|
+
*/
|
|
439
|
+
export function isMermaidSchema(schema) {
|
|
440
|
+
if (!schema || typeof schema !== 'string') {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const trimmedSchema = schema.trim().toLowerCase();
|
|
445
|
+
|
|
446
|
+
// Check for Mermaid-related keywords
|
|
447
|
+
const mermaidIndicators = [
|
|
448
|
+
trimmedSchema.includes('mermaid'),
|
|
449
|
+
trimmedSchema.includes('diagram'),
|
|
450
|
+
trimmedSchema.includes('flowchart'),
|
|
451
|
+
trimmedSchema.includes('sequence'),
|
|
452
|
+
trimmedSchema.includes('gantt'),
|
|
453
|
+
trimmedSchema.includes('pie chart'),
|
|
454
|
+
trimmedSchema.includes('state diagram'),
|
|
455
|
+
trimmedSchema.includes('class diagram'),
|
|
456
|
+
trimmedSchema.includes('entity relationship'),
|
|
457
|
+
trimmedSchema.includes('user journey'),
|
|
458
|
+
trimmedSchema.includes('git graph'),
|
|
459
|
+
trimmedSchema.includes('requirement diagram'),
|
|
460
|
+
trimmedSchema.includes('c4 context')
|
|
461
|
+
];
|
|
462
|
+
|
|
463
|
+
return mermaidIndicators.some(indicator => indicator);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Extract Mermaid diagrams from markdown code blocks with position tracking
|
|
468
|
+
* @param {string} response - Response that may contain markdown with mermaid blocks
|
|
469
|
+
* @returns {Object} - {diagrams: Array<{content: string, fullMatch: string, startIndex: number, endIndex: number}>, cleanedResponse: string}
|
|
470
|
+
*/
|
|
471
|
+
export function extractMermaidFromMarkdown(response) {
|
|
472
|
+
if (!response || typeof response !== 'string') {
|
|
473
|
+
return { diagrams: [], cleanedResponse: response };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Find all mermaid code blocks with enhanced regex to capture more variations
|
|
477
|
+
// This regex captures optional attributes on same line as ```mermaid, and all diagram content
|
|
478
|
+
const mermaidBlockRegex = /```mermaid([^\n]*)\n([\s\S]*?)```/gi;
|
|
479
|
+
const diagrams = [];
|
|
480
|
+
let match;
|
|
481
|
+
|
|
482
|
+
while ((match = mermaidBlockRegex.exec(response)) !== null) {
|
|
483
|
+
const attributes = match[1] ? match[1].trim() : '';
|
|
484
|
+
const fullContent = match[2].trim();
|
|
485
|
+
|
|
486
|
+
// If attributes exist, they were captured separately, so fullContent is just the diagram
|
|
487
|
+
// If no attributes, the first line of fullContent might be diagram type or actual content
|
|
488
|
+
diagrams.push({
|
|
489
|
+
content: fullContent,
|
|
490
|
+
fullMatch: match[0],
|
|
491
|
+
startIndex: match.index,
|
|
492
|
+
endIndex: match.index + match[0].length,
|
|
493
|
+
attributes: attributes
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Return cleaned response (original for now, could be modified if needed)
|
|
498
|
+
return { diagrams, cleanedResponse: response };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Replace mermaid diagrams in original markdown with corrected versions
|
|
503
|
+
* @param {string} originalResponse - Original response with markdown
|
|
504
|
+
* @param {Array} correctedDiagrams - Array of corrected diagram objects
|
|
505
|
+
* @returns {string} - Response with corrected diagrams in original format
|
|
506
|
+
*/
|
|
507
|
+
export function replaceMermaidDiagramsInMarkdown(originalResponse, correctedDiagrams) {
|
|
508
|
+
if (!originalResponse || typeof originalResponse !== 'string') {
|
|
509
|
+
return originalResponse;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (!correctedDiagrams || correctedDiagrams.length === 0) {
|
|
513
|
+
return originalResponse;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
let modifiedResponse = originalResponse;
|
|
517
|
+
|
|
518
|
+
// Sort diagrams by start index in reverse order to preserve indices during replacement
|
|
519
|
+
const sortedDiagrams = [...correctedDiagrams].sort((a, b) => b.startIndex - a.startIndex);
|
|
520
|
+
|
|
521
|
+
for (const diagram of sortedDiagrams) {
|
|
522
|
+
// Reconstruct the code block with original attributes if they existed
|
|
523
|
+
const attributesStr = diagram.attributes ? ` ${diagram.attributes}` : '';
|
|
524
|
+
const newCodeBlock = `\`\`\`mermaid${attributesStr}\n${diagram.content}\n\`\`\``;
|
|
525
|
+
|
|
526
|
+
// Replace the original code block
|
|
527
|
+
modifiedResponse = modifiedResponse.slice(0, diagram.startIndex) +
|
|
528
|
+
newCodeBlock +
|
|
529
|
+
modifiedResponse.slice(diagram.endIndex);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return modifiedResponse;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Validate a single Mermaid diagram
|
|
537
|
+
* @param {string} diagram - Mermaid diagram code
|
|
538
|
+
* @returns {Promise<Object>} - {isValid: boolean, diagramType?: string, error?: string, detailedError?: string}
|
|
539
|
+
*/
|
|
540
|
+
export async function validateMermaidDiagram(diagram) {
|
|
541
|
+
if (!diagram || typeof diagram !== 'string') {
|
|
542
|
+
return { isValid: false, error: 'Empty or invalid diagram input' };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
try {
|
|
546
|
+
const trimmedDiagram = diagram.trim();
|
|
547
|
+
|
|
548
|
+
// Check for markdown code block markers
|
|
549
|
+
if (trimmedDiagram.includes('```')) {
|
|
550
|
+
return {
|
|
551
|
+
isValid: false,
|
|
552
|
+
error: 'Diagram contains markdown code block markers',
|
|
553
|
+
detailedError: 'Mermaid diagram should not contain ``` markers when extracted from markdown'
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Check for common mermaid diagram types (more flexible patterns)
|
|
558
|
+
const diagramPatterns = [
|
|
559
|
+
{ pattern: /^(graph|flowchart)/i, type: 'flowchart' },
|
|
560
|
+
{ pattern: /^sequenceDiagram/i, type: 'sequence' },
|
|
561
|
+
{ pattern: /^gantt/i, type: 'gantt' },
|
|
562
|
+
{ pattern: /^pie/i, type: 'pie' },
|
|
563
|
+
{ pattern: /^stateDiagram/i, type: 'state' },
|
|
564
|
+
{ pattern: /^classDiagram/i, type: 'class' },
|
|
565
|
+
{ pattern: /^erDiagram/i, type: 'er' },
|
|
566
|
+
{ pattern: /^journey/i, type: 'journey' },
|
|
567
|
+
{ pattern: /^gitgraph/i, type: 'gitgraph' },
|
|
568
|
+
{ pattern: /^requirementDiagram/i, type: 'requirement' },
|
|
569
|
+
{ pattern: /^C4Context/i, type: 'c4' },
|
|
570
|
+
];
|
|
571
|
+
|
|
572
|
+
// Find matching diagram type
|
|
573
|
+
let diagramType = null;
|
|
574
|
+
for (const { pattern, type } of diagramPatterns) {
|
|
575
|
+
if (pattern.test(trimmedDiagram)) {
|
|
576
|
+
diagramType = type;
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (!diagramType) {
|
|
582
|
+
return {
|
|
583
|
+
isValid: false,
|
|
584
|
+
error: 'Diagram does not match any known Mermaid diagram pattern',
|
|
585
|
+
detailedError: 'The diagram must start with a valid Mermaid diagram type (graph, sequenceDiagram, gantt, pie, etc.)'
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// GitHub-compatible strict syntax validation
|
|
590
|
+
const lines = trimmedDiagram.split('\n');
|
|
591
|
+
|
|
592
|
+
for (let i = 0; i < lines.length; i++) {
|
|
593
|
+
const line = lines[i].trim();
|
|
594
|
+
if (!line) continue;
|
|
595
|
+
|
|
596
|
+
// Check for GitHub-incompatible patterns that cause "got 'PS'" errors
|
|
597
|
+
if (diagramType === 'flowchart') {
|
|
598
|
+
// Check for unbalanced brackets in node labels
|
|
599
|
+
const brackets = line.match(/\[[^\]]*$/); // Unclosed bracket
|
|
600
|
+
if (brackets) {
|
|
601
|
+
return {
|
|
602
|
+
isValid: false,
|
|
603
|
+
error: `Unclosed bracket on line ${i + 1}`,
|
|
604
|
+
detailedError: `Line "${line}" contains an unclosed bracket`
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// GitHub-strict: Check for parentheses inside node labels (causes PS token error)
|
|
609
|
+
// But allow parentheses inside double-quoted strings
|
|
610
|
+
const nodeWithParens = line.match(/\[[^"\[\]]*\([^"\[\]]*\]/);
|
|
611
|
+
if (nodeWithParens) {
|
|
612
|
+
return {
|
|
613
|
+
isValid: false,
|
|
614
|
+
error: `Parentheses in node label on line ${i + 1} (GitHub incompatible)`,
|
|
615
|
+
detailedError: `Line "${line}" contains parentheses inside node label brackets. GitHub mermaid renderer fails with 'got PS' error. Use quotes or escape characters instead.`
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// GitHub-strict: Check for single quotes and backticks inside node labels (causes PS token error)
|
|
620
|
+
const nodeWithQuotes = line.match(/\{[^{}]*['`][^{}]*\}|\[[^[\]]*['`][^[\]]*\]/);
|
|
621
|
+
if (nodeWithQuotes) {
|
|
622
|
+
const hasBacktick = line.includes('`');
|
|
623
|
+
const quoteType = hasBacktick ? 'backticks' : 'single quotes';
|
|
624
|
+
return {
|
|
625
|
+
isValid: false,
|
|
626
|
+
error: `${hasBacktick ? 'Backticks' : 'Single quotes'} in node label on line ${i + 1} (GitHub incompatible)`,
|
|
627
|
+
detailedError: `Line "${line}" contains ${quoteType} inside node label. GitHub mermaid renderer fails with 'got PS' error. Use double quotes or escape characters instead.`
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// GitHub-strict: Check for complex expressions inside diamond nodes
|
|
632
|
+
// Allow double-quoted strings in diamond nodes, but catch problematic single quotes and complex expressions
|
|
633
|
+
// Allow HTML breaks (<br/>, <br>, etc.) but catch other problematic patterns
|
|
634
|
+
const diamondWithComplexContent = line.match(/\{[^"{}]*[()'"<>&][^"{}]*\}/);
|
|
635
|
+
const hasHtmlBreak = line.match(/\{[^{}]*<br\s*\/?>.*\}/);
|
|
636
|
+
if (diamondWithComplexContent && !line.match(/\{\"[^\"]*\"\}/) && !hasHtmlBreak) {
|
|
637
|
+
return {
|
|
638
|
+
isValid: false,
|
|
639
|
+
error: `Complex expression in diamond node on line ${i + 1} (GitHub incompatible)`,
|
|
640
|
+
detailedError: `Line "${line}" contains special characters in diamond node that may cause GitHub parsing errors. Use simpler text or escape characters.`
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// GitHub-strict: Check for parentheses in subgraph labels (causes PS token error)
|
|
645
|
+
if (line.startsWith('subgraph ') && line.match(/subgraph\s+[^"]*\([^"]*\)/)) {
|
|
646
|
+
return {
|
|
647
|
+
isValid: false,
|
|
648
|
+
error: `Parentheses in subgraph label on line ${i + 1} (GitHub incompatible)`,
|
|
649
|
+
detailedError: `Line "${line}" contains parentheses in subgraph label. GitHub mermaid renderer fails with 'got PS' error. Use quotes around the label or avoid parentheses.`
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (diagramType === 'sequence') {
|
|
655
|
+
// Check for missing colon in sequence messages
|
|
656
|
+
if (line.includes('->>') && !line.includes(':')) {
|
|
657
|
+
return {
|
|
658
|
+
isValid: false,
|
|
659
|
+
error: `Missing colon in sequence message on line ${i + 1}`,
|
|
660
|
+
detailedError: `Line "${line}" appears to be a sequence message but is missing a colon`
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// If we get here, basic validation passed
|
|
667
|
+
return {
|
|
668
|
+
isValid: true,
|
|
669
|
+
diagramType
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
} catch (error) {
|
|
673
|
+
return {
|
|
674
|
+
isValid: false,
|
|
675
|
+
error: error.message || 'Unknown mermaid parsing error',
|
|
676
|
+
detailedError: error.stack || error.toString()
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Validate all Mermaid diagrams in a response
|
|
683
|
+
* @param {string} response - Response that may contain mermaid diagrams
|
|
684
|
+
* @returns {Promise<Object>} - {isValid: boolean, diagrams: Array, errors?: Array}
|
|
685
|
+
*/
|
|
686
|
+
export async function validateMermaidResponse(response) {
|
|
687
|
+
const { diagrams } = extractMermaidFromMarkdown(response);
|
|
688
|
+
|
|
689
|
+
if (diagrams.length === 0) {
|
|
690
|
+
return { isValid: false, diagrams: [], errors: ['No mermaid diagrams found in response'] };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const results = [];
|
|
694
|
+
const errors = [];
|
|
695
|
+
|
|
696
|
+
for (let i = 0; i < diagrams.length; i++) {
|
|
697
|
+
const diagramObj = diagrams[i];
|
|
698
|
+
const validation = await validateMermaidDiagram(diagramObj.content);
|
|
699
|
+
results.push({
|
|
700
|
+
...diagramObj,
|
|
701
|
+
...validation
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
if (!validation.isValid) {
|
|
705
|
+
errors.push(`Diagram ${i + 1}: ${validation.error}`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const isValid = results.every(result => result.isValid);
|
|
710
|
+
|
|
711
|
+
return {
|
|
712
|
+
isValid,
|
|
713
|
+
diagrams: results,
|
|
714
|
+
errors: errors.length > 0 ? errors : undefined
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Create a correction prompt for invalid Mermaid diagrams
|
|
720
|
+
* @param {string} invalidResponse - The response with invalid Mermaid
|
|
721
|
+
* @param {string} schema - The original schema
|
|
722
|
+
* @param {Array} errors - Array of validation errors
|
|
723
|
+
* @param {Array} diagrams - Array of diagram validation results
|
|
724
|
+
* @returns {string} - Correction prompt for the AI
|
|
725
|
+
*/
|
|
726
|
+
export function createMermaidCorrectionPrompt(invalidResponse, schema, errors, diagrams) {
|
|
727
|
+
let prompt = `Your previous response contains invalid Mermaid diagrams that cannot be parsed. Here's what you returned:
|
|
728
|
+
|
|
729
|
+
${invalidResponse}
|
|
730
|
+
|
|
731
|
+
Validation Errors:`;
|
|
732
|
+
|
|
733
|
+
errors.forEach((error, index) => {
|
|
734
|
+
prompt += `\n${index + 1}. ${error}`;
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
if (diagrams && diagrams.length > 0) {
|
|
738
|
+
prompt += `\n\nDiagram Details:`;
|
|
739
|
+
diagrams.forEach((diagramResult, index) => {
|
|
740
|
+
if (!diagramResult.isValid) {
|
|
741
|
+
prompt += `\n\nDiagram ${index + 1}:`;
|
|
742
|
+
const diagramContent = diagramResult.content || diagramResult.diagram || '';
|
|
743
|
+
prompt += `\n- Content: ${diagramContent.substring(0, 100)}${diagramContent.length > 100 ? '...' : ''}`;
|
|
744
|
+
prompt += `\n- Error: ${diagramResult.error}`;
|
|
745
|
+
if (diagramResult.detailedError && diagramResult.detailedError !== diagramResult.error) {
|
|
746
|
+
prompt += `\n- Details: ${diagramResult.detailedError}`;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
prompt += `\n\nPlease correct your response to include valid Mermaid diagrams that match this schema:
|
|
753
|
+
${schema}
|
|
754
|
+
|
|
755
|
+
Ensure all Mermaid diagrams are properly formatted within \`\`\`mermaid code blocks and follow correct Mermaid syntax.`;
|
|
756
|
+
|
|
757
|
+
return prompt;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Specialized Mermaid diagram fixing agent
|
|
762
|
+
* Uses a separate ProbeAgent instance optimized for Mermaid syntax correction
|
|
763
|
+
*/
|
|
764
|
+
export class MermaidFixingAgent {
|
|
765
|
+
constructor(options = {}) {
|
|
766
|
+
// Import ProbeAgent dynamically to avoid circular dependencies
|
|
767
|
+
this.ProbeAgent = null;
|
|
768
|
+
this.options = {
|
|
769
|
+
sessionId: options.sessionId || `mermaid-fixer-${Date.now()}`,
|
|
770
|
+
path: options.path || process.cwd(),
|
|
771
|
+
provider: options.provider,
|
|
772
|
+
model: options.model,
|
|
773
|
+
debug: options.debug,
|
|
774
|
+
tracer: options.tracer,
|
|
775
|
+
// Set to false since we're only fixing syntax, not implementing code
|
|
776
|
+
allowEdit: false
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Get the specialized prompt for mermaid diagram fixing
|
|
782
|
+
*/
|
|
783
|
+
getMermaidFixingPrompt() {
|
|
784
|
+
return `You are a world-class Mermaid diagram syntax correction specialist. Your expertise lies in analyzing and fixing Mermaid diagram syntax errors while preserving the original intent, structure, and semantic meaning.
|
|
785
|
+
|
|
786
|
+
CORE RESPONSIBILITIES:
|
|
787
|
+
- Analyze Mermaid diagrams for syntax errors and structural issues
|
|
788
|
+
- Fix syntax errors while maintaining the original diagram's logical flow
|
|
789
|
+
- Ensure diagrams follow proper Mermaid syntax rules and best practices
|
|
790
|
+
- Handle all diagram types: flowchart, sequence, gantt, pie, state, class, er, journey, gitgraph, requirement, c4
|
|
791
|
+
|
|
792
|
+
MERMAID DIAGRAM TYPES & SYNTAX RULES:
|
|
793
|
+
1. **Flowchart/Graph**: Start with 'graph' or 'flowchart', use proper node definitions and arrows
|
|
794
|
+
2. **Sequence**: Start with 'sequenceDiagram', use proper participant and message syntax
|
|
795
|
+
3. **Gantt**: Start with 'gantt', use proper date formats and task definitions
|
|
796
|
+
4. **State**: Start with 'stateDiagram-v2', use proper state transitions
|
|
797
|
+
5. **Class**: Start with 'classDiagram', use proper class and relationship syntax
|
|
798
|
+
6. **Entity-Relationship**: Start with 'erDiagram', use proper entity and relationship syntax
|
|
799
|
+
|
|
800
|
+
FIXING METHODOLOGY:
|
|
801
|
+
1. **Identify diagram type** from the first line or content analysis
|
|
802
|
+
2. **Validate syntax** against Mermaid specification for that diagram type
|
|
803
|
+
3. **Fix errors systematically**:
|
|
804
|
+
- Unclosed brackets, parentheses, or quotes
|
|
805
|
+
- Missing or incorrect arrows and connectors
|
|
806
|
+
- Invalid node IDs or labels
|
|
807
|
+
- Incorrect formatting for diagram-specific elements
|
|
808
|
+
- **Parentheses in node labels or subgraph names**: Wrap text containing parentheses in double quotes to prevent GitHub parsing errors
|
|
809
|
+
- Single quotes in node labels (GitHub's parser expects double quotes)
|
|
810
|
+
4. **Preserve semantic meaning** - never change the intended flow or relationships
|
|
811
|
+
5. **Use proper escaping** for special characters and spaces
|
|
812
|
+
6. **Ensure consistency** in naming conventions and formatting
|
|
813
|
+
|
|
814
|
+
CRITICAL RULES:
|
|
815
|
+
- ALWAYS output only the corrected Mermaid code within a \`\`\`mermaid code block
|
|
816
|
+
- NEVER add explanations, comments, or additional text outside the code block
|
|
817
|
+
- PRESERVE the original diagram's intended meaning and flow
|
|
818
|
+
- FIX syntax errors without changing the logical structure
|
|
819
|
+
- ENSURE the output is valid, parseable Mermaid syntax
|
|
820
|
+
- WRAP text containing parentheses in double quotes for GitHub compatibility
|
|
821
|
+
|
|
822
|
+
When presented with a broken Mermaid diagram, analyze it thoroughly and provide the corrected version that maintains the original intent while fixing all syntax issues.`;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Initialize the ProbeAgent if not already done
|
|
827
|
+
*/
|
|
828
|
+
async initializeAgent() {
|
|
829
|
+
if (!this.ProbeAgent) {
|
|
830
|
+
// Dynamic import to avoid circular dependency
|
|
831
|
+
const { ProbeAgent } = await import('./ProbeAgent.js');
|
|
832
|
+
this.ProbeAgent = ProbeAgent;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (!this.agent) {
|
|
836
|
+
this.agent = new this.ProbeAgent({
|
|
837
|
+
sessionId: this.options.sessionId,
|
|
838
|
+
customPrompt: this.getMermaidFixingPrompt(),
|
|
839
|
+
path: this.options.path,
|
|
840
|
+
provider: this.options.provider,
|
|
841
|
+
model: this.options.model,
|
|
842
|
+
debug: this.options.debug,
|
|
843
|
+
tracer: this.options.tracer,
|
|
844
|
+
allowEdit: this.options.allowEdit
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
return this.agent;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Fix a single Mermaid diagram using the specialized agent
|
|
853
|
+
* @param {string} diagramContent - The broken Mermaid diagram content
|
|
854
|
+
* @param {Array} originalErrors - Array of errors detected in the original diagram
|
|
855
|
+
* @param {Object} diagramInfo - Additional context about the diagram (type, position, etc.)
|
|
856
|
+
* @returns {Promise<string>} - The corrected Mermaid diagram
|
|
857
|
+
*/
|
|
858
|
+
async fixMermaidDiagram(diagramContent, originalErrors = [], diagramInfo = {}) {
|
|
859
|
+
// First, try auto-fixing HTML entities without AI
|
|
860
|
+
const decodedContent = decodeHtmlEntities(diagramContent);
|
|
861
|
+
|
|
862
|
+
// If HTML entity decoding changed the content, validate it first
|
|
863
|
+
if (decodedContent !== diagramContent) {
|
|
864
|
+
try {
|
|
865
|
+
const quickValidation = await validateMermaidDiagram(decodedContent);
|
|
866
|
+
if (quickValidation.isValid) {
|
|
867
|
+
// HTML entity decoding fixed the issue, no need for AI
|
|
868
|
+
if (this.options.debug) {
|
|
869
|
+
console.error('[DEBUG] Fixed Mermaid diagram with HTML entity decoding only');
|
|
870
|
+
}
|
|
871
|
+
return decodedContent;
|
|
872
|
+
}
|
|
873
|
+
} catch (error) {
|
|
874
|
+
// If validation fails, continue with AI fixing using decoded content
|
|
875
|
+
if (this.options.debug) {
|
|
876
|
+
console.error('[DEBUG] HTML entity decoding didn\'t fully fix diagram, continuing with AI fixing');
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
await this.initializeAgent();
|
|
882
|
+
|
|
883
|
+
const errorContext = originalErrors.length > 0
|
|
884
|
+
? `\n\nDetected errors: ${originalErrors.join(', ')}`
|
|
885
|
+
: '';
|
|
886
|
+
|
|
887
|
+
const diagramTypeHint = diagramInfo.diagramType
|
|
888
|
+
? `\n\nExpected diagram type: ${diagramInfo.diagramType}`
|
|
889
|
+
: '';
|
|
890
|
+
|
|
891
|
+
// Use decoded content for AI fixing to ensure HTML entities are handled
|
|
892
|
+
const contentToFix = decodedContent !== diagramContent ? decodedContent : diagramContent;
|
|
893
|
+
|
|
894
|
+
const prompt = `Analyze and fix the following Mermaid diagram.${errorContext}${diagramTypeHint}
|
|
895
|
+
|
|
896
|
+
Broken Mermaid diagram:
|
|
897
|
+
\`\`\`mermaid
|
|
898
|
+
${contentToFix}
|
|
899
|
+
\`\`\`
|
|
900
|
+
|
|
901
|
+
Provide only the corrected Mermaid diagram within a mermaid code block. Do not add any explanations or additional text.`;
|
|
902
|
+
|
|
903
|
+
try {
|
|
904
|
+
const result = await this.agent.answer(prompt, [], {
|
|
905
|
+
schema: 'Return only valid Mermaid diagram code within ```mermaid code block'
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
// Extract the mermaid code from the response
|
|
909
|
+
const extractedDiagram = this.extractCorrectedDiagram(result);
|
|
910
|
+
return extractedDiagram || result;
|
|
911
|
+
} catch (error) {
|
|
912
|
+
if (this.options.debug) {
|
|
913
|
+
console.error(`[DEBUG] Mermaid fixing failed: ${error.message}`);
|
|
914
|
+
}
|
|
915
|
+
throw new Error(`Failed to fix Mermaid diagram: ${error.message}`);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Extract the corrected diagram from the agent's response
|
|
921
|
+
* @param {string} response - The agent's response
|
|
922
|
+
* @returns {string} - The extracted mermaid diagram
|
|
923
|
+
*/
|
|
924
|
+
extractCorrectedDiagram(response) {
|
|
925
|
+
// Try to extract mermaid code block
|
|
926
|
+
const mermaidMatch = response.match(/```mermaid\s*\n([\s\S]*?)\n```/);
|
|
927
|
+
if (mermaidMatch) {
|
|
928
|
+
return mermaidMatch[1].trim();
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Fallback: try to extract any code block
|
|
932
|
+
const codeMatch = response.match(/```\s*\n([\s\S]*?)\n```/);
|
|
933
|
+
if (codeMatch) {
|
|
934
|
+
return codeMatch[1].trim();
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// If no code blocks found, return the response as-is (cleaned)
|
|
938
|
+
return response.replace(/```\w*\n?/g, '').replace(/\n?```/g, '').trim();
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Get token usage information from the specialized agent
|
|
943
|
+
* @returns {Object} - Token usage statistics
|
|
944
|
+
*/
|
|
945
|
+
getTokenUsage() {
|
|
946
|
+
return this.agent ? this.agent.getTokenUsage() : null;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Cancel any ongoing operations
|
|
951
|
+
*/
|
|
952
|
+
cancel() {
|
|
953
|
+
if (this.agent) {
|
|
954
|
+
this.agent.cancel();
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* Enhanced Mermaid validation with specialized agent fixing
|
|
961
|
+
* @param {string} response - Response that may contain mermaid diagrams
|
|
962
|
+
* @param {Object} options - Options for validation and fixing
|
|
963
|
+
* @returns {Promise<Object>} - Enhanced validation result with fixing capability
|
|
964
|
+
*/
|
|
965
|
+
export async function validateAndFixMermaidResponse(response, options = {}) {
|
|
966
|
+
const { schema, debug, path, provider, model, tracer } = options;
|
|
967
|
+
const startTime = Date.now();
|
|
968
|
+
|
|
969
|
+
if (debug) {
|
|
970
|
+
console.log(`[DEBUG] Mermaid validation: Starting enhanced validation for response (${response.length} chars)`);
|
|
971
|
+
console.log(`[DEBUG] Mermaid validation: Options - path: ${path}, provider: ${provider}, model: ${model}`);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/**
|
|
975
|
+
* Helper function to determine if node content needs quoting due to problematic characters
|
|
976
|
+
* @param {string} content - The node content to check
|
|
977
|
+
* @returns {boolean} - True if content needs to be quoted
|
|
978
|
+
*/
|
|
979
|
+
const needsQuoting = (content) => {
|
|
980
|
+
return /[()'"<>&`]/.test(content) || // Core problematic characters
|
|
981
|
+
content.includes('e.g.') ||
|
|
982
|
+
content.includes('i.e.') ||
|
|
983
|
+
content.includes('src/') ||
|
|
984
|
+
content.includes('defaults/') ||
|
|
985
|
+
content.includes('.ts') ||
|
|
986
|
+
content.includes('.js') ||
|
|
987
|
+
content.includes('.yaml') ||
|
|
988
|
+
content.includes('.json') ||
|
|
989
|
+
content.includes('.md') ||
|
|
990
|
+
content.includes('.html') ||
|
|
991
|
+
content.includes('.css');
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
// Record mermaid validation start in telemetry
|
|
995
|
+
if (tracer) {
|
|
996
|
+
tracer.recordMermaidValidationEvent('started', {
|
|
997
|
+
'mermaid_validation.response_length': response.length,
|
|
998
|
+
'mermaid_validation.provider': provider,
|
|
999
|
+
'mermaid_validation.model': model
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// First, run standard validation
|
|
1004
|
+
const validationStart = Date.now();
|
|
1005
|
+
const validation = await validateMermaidResponse(response);
|
|
1006
|
+
const validationTime = Date.now() - validationStart;
|
|
1007
|
+
|
|
1008
|
+
if (debug) {
|
|
1009
|
+
console.log(`[DEBUG] Mermaid validation: Initial validation completed in ${validationTime}ms`);
|
|
1010
|
+
console.log(`[DEBUG] Mermaid validation: Found ${validation.diagrams?.length || 0} diagrams, valid: ${validation.isValid}`);
|
|
1011
|
+
if (validation.diagrams) {
|
|
1012
|
+
validation.diagrams.forEach((diag, i) => {
|
|
1013
|
+
console.log(`[DEBUG] Mermaid validation: Diagram ${i + 1}: ${diag.isValid ? 'valid' : 'invalid'} (${diag.diagramType || 'unknown type'})`);
|
|
1014
|
+
if (!diag.isValid) {
|
|
1015
|
+
console.log(`[DEBUG] Mermaid validation: Error for diagram ${i + 1}: ${diag.error}`);
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// Always check for HTML entities, even if diagrams are technically valid
|
|
1022
|
+
let needsHtmlEntityCheck = false;
|
|
1023
|
+
if (validation.diagrams && validation.diagrams.length > 0) {
|
|
1024
|
+
for (const diagram of validation.diagrams) {
|
|
1025
|
+
if (diagram.content && (diagram.content.includes('<') || diagram.content.includes('>') || diagram.content.includes('&') || diagram.content.includes('"') || diagram.content.includes('''))) {
|
|
1026
|
+
needsHtmlEntityCheck = true;
|
|
1027
|
+
break;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (validation.isValid && !needsHtmlEntityCheck) {
|
|
1033
|
+
if (debug) {
|
|
1034
|
+
console.log(`[DEBUG] Mermaid validation: All diagrams valid and no HTML entities found, no fixing needed`);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Record successful validation in telemetry
|
|
1038
|
+
if (tracer) {
|
|
1039
|
+
tracer.recordMermaidValidationEvent('completed', {
|
|
1040
|
+
'mermaid_validation.success': true,
|
|
1041
|
+
'mermaid_validation.diagrams_found': validation.diagrams?.length || 0,
|
|
1042
|
+
'mermaid_validation.fixes_needed': false,
|
|
1043
|
+
'mermaid_validation.duration_ms': Date.now() - startTime
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// All diagrams are valid and no HTML entities found, no fixing needed
|
|
1048
|
+
return {
|
|
1049
|
+
...validation,
|
|
1050
|
+
wasFixed: false,
|
|
1051
|
+
originalResponse: response,
|
|
1052
|
+
fixedResponse: response
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// If no diagrams found at all, return without attempting to fix
|
|
1057
|
+
if (!validation.diagrams || validation.diagrams.length === 0) {
|
|
1058
|
+
if (debug) {
|
|
1059
|
+
console.log(`[DEBUG] Mermaid validation: No mermaid diagrams found in response, skipping fixes`);
|
|
1060
|
+
}
|
|
1061
|
+
return {
|
|
1062
|
+
...validation,
|
|
1063
|
+
wasFixed: false,
|
|
1064
|
+
originalResponse: response,
|
|
1065
|
+
fixedResponse: response
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// Try HTML entity decoding auto-fix (for both invalid diagrams and valid diagrams with HTML entities)
|
|
1070
|
+
const invalidCount = validation.diagrams.filter(d => !d.isValid).length;
|
|
1071
|
+
if (debug) {
|
|
1072
|
+
if (invalidCount > 0) {
|
|
1073
|
+
console.log(`[DEBUG] Mermaid validation: ${invalidCount} invalid diagrams detected, trying HTML entity auto-fix first...`);
|
|
1074
|
+
} else {
|
|
1075
|
+
console.log(`[DEBUG] Mermaid validation: Diagrams are valid but HTML entities detected, applying HTML entity auto-fix...`);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
try {
|
|
1080
|
+
let fixedResponse = response;
|
|
1081
|
+
const fixingResults = [];
|
|
1082
|
+
let htmlEntityFixesApplied = false;
|
|
1083
|
+
|
|
1084
|
+
// Extract diagrams with position information for replacement
|
|
1085
|
+
const { diagrams } = extractMermaidFromMarkdown(response);
|
|
1086
|
+
|
|
1087
|
+
// First pass: Try HTML entity decoding on ALL diagrams (not just invalid ones)
|
|
1088
|
+
// HTML entities in mermaid diagrams are almost always unintended, even if the diagram is technically valid
|
|
1089
|
+
const allDiagrams = validation.diagrams
|
|
1090
|
+
.map((result, index) => ({ ...result, originalIndex: index }))
|
|
1091
|
+
.reverse();
|
|
1092
|
+
|
|
1093
|
+
for (const diagram of allDiagrams) {
|
|
1094
|
+
const originalContent = diagram.content;
|
|
1095
|
+
const decodedContent = decodeHtmlEntities(originalContent);
|
|
1096
|
+
|
|
1097
|
+
if (decodedContent !== originalContent) {
|
|
1098
|
+
// HTML entities were found and decoded, validate the result
|
|
1099
|
+
try {
|
|
1100
|
+
const quickValidation = await validateMermaidDiagram(decodedContent);
|
|
1101
|
+
if (quickValidation.isValid) {
|
|
1102
|
+
// HTML entity decoding improved this diagram!
|
|
1103
|
+
const originalDiagram = diagrams[diagram.originalIndex];
|
|
1104
|
+
const attributesStr = originalDiagram.attributes ? ` ${originalDiagram.attributes}` : '';
|
|
1105
|
+
const newCodeBlock = `\`\`\`mermaid${attributesStr}\n${decodedContent}\n\`\`\``;
|
|
1106
|
+
|
|
1107
|
+
fixedResponse = fixedResponse.slice(0, originalDiagram.startIndex) +
|
|
1108
|
+
newCodeBlock +
|
|
1109
|
+
fixedResponse.slice(originalDiagram.endIndex);
|
|
1110
|
+
|
|
1111
|
+
fixingResults.push({
|
|
1112
|
+
diagramIndex: diagram.originalIndex,
|
|
1113
|
+
wasFixed: true,
|
|
1114
|
+
originalContent: originalContent,
|
|
1115
|
+
fixedContent: decodedContent,
|
|
1116
|
+
originalError: diagram.error || 'HTML entity cleanup',
|
|
1117
|
+
fixedWithHtmlDecoding: true
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
htmlEntityFixesApplied = true;
|
|
1121
|
+
|
|
1122
|
+
if (debug) {
|
|
1123
|
+
console.log(`[DEBUG] Mermaid validation: Fixed diagram ${diagram.originalIndex + 1} with HTML entity decoding`);
|
|
1124
|
+
console.log(`[DEBUG] Mermaid validation: Original status: ${diagram.isValid ? 'valid' : 'invalid'} - ${diagram.error || 'no error'}`);
|
|
1125
|
+
console.log(`[DEBUG] Mermaid validation: Decoded HTML entities`);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
if (debug) {
|
|
1130
|
+
console.log(`[DEBUG] Mermaid validation: HTML entity decoding didn't improve diagram ${diagram.originalIndex + 1}: ${error.message}`);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// If HTML entity fixes were applied, re-validate the entire response
|
|
1137
|
+
if (htmlEntityFixesApplied) {
|
|
1138
|
+
const revalidation = await validateMermaidResponse(fixedResponse);
|
|
1139
|
+
if (revalidation.isValid) {
|
|
1140
|
+
// All diagrams are now valid, return without AI fixing
|
|
1141
|
+
const totalTime = Date.now() - startTime;
|
|
1142
|
+
if (debug) {
|
|
1143
|
+
console.log(`[DEBUG] Mermaid validation: All diagrams fixed with HTML entity decoding in ${totalTime}ms, no AI needed`);
|
|
1144
|
+
console.log(`[DEBUG] Mermaid validation: Applied ${fixingResults.length} HTML entity fixes`);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Record HTML entity fix success in telemetry
|
|
1148
|
+
if (tracer) {
|
|
1149
|
+
tracer.recordMermaidValidationEvent('html_fix_completed', {
|
|
1150
|
+
'mermaid_validation.success': true,
|
|
1151
|
+
'mermaid_validation.fix_method': 'html_entity_decoding',
|
|
1152
|
+
'mermaid_validation.diagrams_fixed': fixingResults.length,
|
|
1153
|
+
'mermaid_validation.duration_ms': totalTime
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
return {
|
|
1157
|
+
...revalidation,
|
|
1158
|
+
wasFixed: true,
|
|
1159
|
+
originalResponse: response,
|
|
1160
|
+
fixedResponse: fixedResponse,
|
|
1161
|
+
fixingResults: fixingResults
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// Proactive pass: Fix common node label issues in ALL diagrams (not just invalid ones)
|
|
1167
|
+
let proactiveFixesApplied = false;
|
|
1168
|
+
|
|
1169
|
+
// Re-extract diagrams after HTML entity fixes
|
|
1170
|
+
const { diagrams: currentDiagrams } = extractMermaidFromMarkdown(fixedResponse);
|
|
1171
|
+
|
|
1172
|
+
for (let diagramIndex = currentDiagrams.length - 1; diagramIndex >= 0; diagramIndex--) {
|
|
1173
|
+
const diagram = currentDiagrams[diagramIndex];
|
|
1174
|
+
const originalContent = diagram.content;
|
|
1175
|
+
const lines = originalContent.split('\n');
|
|
1176
|
+
let wasFixed = false;
|
|
1177
|
+
|
|
1178
|
+
// Proactively fix node labels that contain special characters
|
|
1179
|
+
const fixedLines = lines.map(line => {
|
|
1180
|
+
const trimmedLine = line.trim();
|
|
1181
|
+
let modifiedLine = line;
|
|
1182
|
+
|
|
1183
|
+
// Enhanced auto-fixing for square bracket nodes [...]
|
|
1184
|
+
if (trimmedLine.match(/\[[^\]]*\]/)) {
|
|
1185
|
+
modifiedLine = modifiedLine.replace(/\[([^\]]*)\]/g, (match, content) => {
|
|
1186
|
+
// Skip if already properly quoted
|
|
1187
|
+
if (content.trim().startsWith('"') && content.trim().endsWith('"')) {
|
|
1188
|
+
return match;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// Check if content needs quoting (contains problematic patterns)
|
|
1192
|
+
if (needsQuoting(content)) {
|
|
1193
|
+
wasFixed = true;
|
|
1194
|
+
// Replace internal double quotes with single quotes to avoid nesting
|
|
1195
|
+
const safeContent = content.replace(/"/g, "'");
|
|
1196
|
+
return `["${safeContent}"]`;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
return match;
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Enhanced auto-fixing for diamond nodes {...}
|
|
1204
|
+
if (trimmedLine.match(/\{[^{}]*\}/)) {
|
|
1205
|
+
modifiedLine = modifiedLine.replace(/\{([^{}]*)\}/g, (match, content) => {
|
|
1206
|
+
// Skip if already properly quoted
|
|
1207
|
+
if (content.trim().startsWith('"') && content.trim().endsWith('"')) {
|
|
1208
|
+
return match;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// Check if content needs quoting (contains problematic patterns)
|
|
1212
|
+
if (needsQuoting(content)) {
|
|
1213
|
+
wasFixed = true;
|
|
1214
|
+
// Replace internal double quotes with single quotes to avoid nesting
|
|
1215
|
+
const safeContent = content.replace(/"/g, "'");
|
|
1216
|
+
return `{"${safeContent}"}`;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
return match;
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
return modifiedLine;
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
if (wasFixed) {
|
|
1227
|
+
const fixedContent = fixedLines.join('\n');
|
|
1228
|
+
|
|
1229
|
+
// Replace the diagram in the response
|
|
1230
|
+
const attributesStr = diagram.attributes ? ` ${diagram.attributes}` : '';
|
|
1231
|
+
const newCodeBlock = `\`\`\`mermaid${attributesStr}\n${fixedContent}\n\`\`\``;
|
|
1232
|
+
|
|
1233
|
+
fixedResponse = fixedResponse.slice(0, diagram.startIndex) +
|
|
1234
|
+
newCodeBlock +
|
|
1235
|
+
fixedResponse.slice(diagram.endIndex);
|
|
1236
|
+
|
|
1237
|
+
fixingResults.push({
|
|
1238
|
+
diagramIndex: diagramIndex,
|
|
1239
|
+
wasFixed: true,
|
|
1240
|
+
originalContent: originalContent,
|
|
1241
|
+
fixedContent: fixedContent,
|
|
1242
|
+
originalError: 'Proactive node label quoting',
|
|
1243
|
+
fixMethod: 'node_label_quote_wrapping',
|
|
1244
|
+
fixedWithProactiveQuoting: true
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
proactiveFixesApplied = true;
|
|
1248
|
+
|
|
1249
|
+
if (debug) {
|
|
1250
|
+
console.log(`[DEBUG] Mermaid validation: Proactively fixed diagram ${diagramIndex + 1} with node label quoting`);
|
|
1251
|
+
console.log(`[DEBUG] Mermaid validation: Applied automatic quoting to special characters`);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// If proactive fixes were applied, re-validate the entire response
|
|
1257
|
+
if (proactiveFixesApplied) {
|
|
1258
|
+
const revalidation = await validateMermaidResponse(fixedResponse);
|
|
1259
|
+
if (revalidation.isValid) {
|
|
1260
|
+
// All diagrams are now valid, return without AI fixing
|
|
1261
|
+
const totalTime = Date.now() - startTime;
|
|
1262
|
+
if (debug) {
|
|
1263
|
+
console.log(`[DEBUG] Mermaid validation: All diagrams fixed with proactive quoting in ${totalTime}ms, no AI needed`);
|
|
1264
|
+
console.log(`[DEBUG] Mermaid validation: Applied ${fixingResults.length} proactive fixes`);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// Record proactive fix success in telemetry
|
|
1268
|
+
if (tracer) {
|
|
1269
|
+
tracer.recordMermaidValidationEvent('proactive_fix_completed', {
|
|
1270
|
+
'mermaid_validation.success': true,
|
|
1271
|
+
'mermaid_validation.fix_method': 'node_label_quote_wrapping',
|
|
1272
|
+
'mermaid_validation.diagrams_fixed': fixingResults.length,
|
|
1273
|
+
'mermaid_validation.duration_ms': totalTime
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
return {
|
|
1277
|
+
...revalidation,
|
|
1278
|
+
wasFixed: true,
|
|
1279
|
+
originalResponse: response,
|
|
1280
|
+
fixedResponse: fixedResponse,
|
|
1281
|
+
fixingResults: fixingResults,
|
|
1282
|
+
performanceMetrics: {
|
|
1283
|
+
totalTimeMs: totalTime,
|
|
1284
|
+
aiFixingTimeMs: 0,
|
|
1285
|
+
finalValidationTimeMs: 0,
|
|
1286
|
+
diagramsProcessed: fixingResults.length,
|
|
1287
|
+
diagramsFixed: fixingResults.length
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// Second pass: Try auto-fixing unquoted subgraph names with parentheses
|
|
1294
|
+
let subgraphFixesApplied = false;
|
|
1295
|
+
|
|
1296
|
+
// Re-extract diagrams and re-validate after HTML entity fixes
|
|
1297
|
+
const { diagrams: postHtmlDiagrams } = extractMermaidFromMarkdown(fixedResponse);
|
|
1298
|
+
const postHtmlValidation = await validateMermaidResponse(fixedResponse);
|
|
1299
|
+
|
|
1300
|
+
const stillInvalidAfterHtml = postHtmlValidation.diagrams
|
|
1301
|
+
.map((result, index) => ({ ...result, originalIndex: index }))
|
|
1302
|
+
.filter(result => !result.isValid)
|
|
1303
|
+
.reverse();
|
|
1304
|
+
|
|
1305
|
+
for (const invalidDiagram of stillInvalidAfterHtml) {
|
|
1306
|
+
// Check if this is a subgraph parentheses error that we can auto-fix
|
|
1307
|
+
if (invalidDiagram.error && invalidDiagram.error.includes('Parentheses in subgraph label')) {
|
|
1308
|
+
const originalContent = invalidDiagram.content;
|
|
1309
|
+
const lines = originalContent.split('\n');
|
|
1310
|
+
let wasFixed = false;
|
|
1311
|
+
|
|
1312
|
+
// Find and fix unquoted subgraph lines with parentheses
|
|
1313
|
+
const fixedLines = lines.map(line => {
|
|
1314
|
+
const trimmedLine = line.trim();
|
|
1315
|
+
if (trimmedLine.startsWith('subgraph ') &&
|
|
1316
|
+
!trimmedLine.match(/subgraph\s+"[^"]*"/) && // not already quoted
|
|
1317
|
+
trimmedLine.match(/subgraph\s+[^"]*\([^"]*\)/)) { // has unquoted parentheses
|
|
1318
|
+
|
|
1319
|
+
// Extract the subgraph name part
|
|
1320
|
+
const match = trimmedLine.match(/^(\s*subgraph\s+)(.+)$/);
|
|
1321
|
+
if (match) {
|
|
1322
|
+
const prefix = match[1];
|
|
1323
|
+
const name = match[2];
|
|
1324
|
+
const fixedLine = line.replace(trimmedLine, `${prefix.trim()} "${name}"`);
|
|
1325
|
+
wasFixed = true;
|
|
1326
|
+
return fixedLine;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
return line;
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
if (wasFixed) {
|
|
1333
|
+
const fixedContent = fixedLines.join('\n');
|
|
1334
|
+
|
|
1335
|
+
// Validate the fixed content
|
|
1336
|
+
try {
|
|
1337
|
+
const quickValidation = await validateMermaidDiagram(fixedContent);
|
|
1338
|
+
if (quickValidation.isValid) {
|
|
1339
|
+
// Subgraph auto-fix worked!
|
|
1340
|
+
const originalDiagram = postHtmlDiagrams[invalidDiagram.originalIndex];
|
|
1341
|
+
const attributesStr = originalDiagram.attributes ? ` ${originalDiagram.attributes}` : '';
|
|
1342
|
+
const newCodeBlock = `\`\`\`mermaid${attributesStr}\n${fixedContent}\n\`\`\``;
|
|
1343
|
+
|
|
1344
|
+
fixedResponse = fixedResponse.slice(0, originalDiagram.startIndex) +
|
|
1345
|
+
newCodeBlock +
|
|
1346
|
+
fixedResponse.slice(originalDiagram.endIndex);
|
|
1347
|
+
|
|
1348
|
+
fixingResults.push({
|
|
1349
|
+
originalIndex: invalidDiagram.originalIndex,
|
|
1350
|
+
wasFixed: true,
|
|
1351
|
+
originalError: invalidDiagram.error,
|
|
1352
|
+
fixMethod: 'subgraph_quote_wrapping',
|
|
1353
|
+
fixedWithSubgraphQuoting: true
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
subgraphFixesApplied = true;
|
|
1357
|
+
|
|
1358
|
+
if (debug) {
|
|
1359
|
+
console.log(`[DEBUG] Mermaid validation: Fixed diagram ${invalidDiagram.originalIndex + 1} with subgraph quote wrapping`);
|
|
1360
|
+
console.log(`[DEBUG] Mermaid validation: Original error: ${invalidDiagram.error}`);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
} catch (error) {
|
|
1364
|
+
if (debug) {
|
|
1365
|
+
console.log(`[DEBUG] Mermaid validation: Subgraph auto-fix didn't work for diagram ${invalidDiagram.originalIndex + 1}: ${error.message}`);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// If subgraph fixes were applied, re-validate the entire response
|
|
1373
|
+
if (subgraphFixesApplied) {
|
|
1374
|
+
const revalidation = await validateMermaidResponse(fixedResponse);
|
|
1375
|
+
if (revalidation.isValid) {
|
|
1376
|
+
// All diagrams are now valid, return without AI fixing
|
|
1377
|
+
const totalTime = Date.now() - startTime;
|
|
1378
|
+
if (debug) {
|
|
1379
|
+
console.log(`[DEBUG] Mermaid validation: All diagrams fixed with auto-fixes in ${totalTime}ms, no AI needed`);
|
|
1380
|
+
console.log(`[DEBUG] Mermaid validation: Applied ${fixingResults.length} auto-fixes (HTML entities + subgraph quotes)`);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// Record auto-fix success in telemetry
|
|
1384
|
+
if (tracer) {
|
|
1385
|
+
tracer.recordMermaidValidationEvent('auto_fix_completed', {
|
|
1386
|
+
'mermaid_validation.success': true,
|
|
1387
|
+
'mermaid_validation.fix_method': 'auto_fixes',
|
|
1388
|
+
'mermaid_validation.diagrams_fixed': fixingResults.length,
|
|
1389
|
+
'mermaid_validation.duration_ms': totalTime
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
return {
|
|
1393
|
+
...revalidation,
|
|
1394
|
+
wasFixed: true,
|
|
1395
|
+
originalResponse: response,
|
|
1396
|
+
fixedResponse: fixedResponse,
|
|
1397
|
+
fixingResults: fixingResults,
|
|
1398
|
+
performanceMetrics: {
|
|
1399
|
+
totalTimeMs: totalTime,
|
|
1400
|
+
aiFixingTimeMs: 0,
|
|
1401
|
+
finalValidationTimeMs: 0,
|
|
1402
|
+
diagramsProcessed: fixingResults.length,
|
|
1403
|
+
diagramsFixed: fixingResults.length
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// Third pass: Try auto-fixing node labels with parentheses or single quotes
|
|
1410
|
+
let nodeLabelFixesApplied = false;
|
|
1411
|
+
|
|
1412
|
+
// Re-extract diagrams and re-validate after previous fixes
|
|
1413
|
+
const { diagrams: postSubgraphDiagrams } = extractMermaidFromMarkdown(fixedResponse);
|
|
1414
|
+
const postSubgraphValidation = await validateMermaidResponse(fixedResponse);
|
|
1415
|
+
|
|
1416
|
+
const stillInvalidAfterSubgraph = postSubgraphValidation.diagrams
|
|
1417
|
+
.map((result, index) => ({ ...result, originalIndex: index }))
|
|
1418
|
+
.filter(result => !result.isValid)
|
|
1419
|
+
.reverse();
|
|
1420
|
+
|
|
1421
|
+
for (const invalidDiagram of stillInvalidAfterSubgraph) {
|
|
1422
|
+
// Check if this is a node label error that we can auto-fix
|
|
1423
|
+
if (invalidDiagram.error &&
|
|
1424
|
+
(invalidDiagram.error.includes('Parentheses in node label') ||
|
|
1425
|
+
invalidDiagram.error.includes('Complex expression in diamond node') ||
|
|
1426
|
+
invalidDiagram.error.includes('Single quotes in node label') ||
|
|
1427
|
+
invalidDiagram.error.includes('Backticks in node label'))) {
|
|
1428
|
+
const originalContent = invalidDiagram.content;
|
|
1429
|
+
const lines = originalContent.split('\n');
|
|
1430
|
+
let wasFixed = false;
|
|
1431
|
+
|
|
1432
|
+
// Find and fix node labels with special characters that need quoting
|
|
1433
|
+
const fixedLines = lines.map(line => {
|
|
1434
|
+
const trimmedLine = line.trim();
|
|
1435
|
+
let modifiedLine = line;
|
|
1436
|
+
|
|
1437
|
+
// Enhanced auto-fixing for square bracket nodes [...]
|
|
1438
|
+
// Look for any node labels that contain special characters and aren't already quoted
|
|
1439
|
+
if (trimmedLine.match(/\[[^\]]*\]/)) {
|
|
1440
|
+
modifiedLine = modifiedLine.replace(/\[([^\]]*)\]/g, (match, content) => {
|
|
1441
|
+
// Skip if already properly quoted
|
|
1442
|
+
if (content.trim().startsWith('"') && content.trim().endsWith('"')) {
|
|
1443
|
+
return match;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// Check if content needs quoting (contains problematic patterns)
|
|
1447
|
+
if (needsQuoting(content)) {
|
|
1448
|
+
wasFixed = true;
|
|
1449
|
+
// Replace internal double quotes with single quotes to avoid nesting
|
|
1450
|
+
const safeContent = content.replace(/"/g, "'");
|
|
1451
|
+
return `["${safeContent}"]`;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
return match;
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// Enhanced auto-fixing for diamond nodes {...}
|
|
1459
|
+
if (trimmedLine.match(/\{[^{}]*\}/)) {
|
|
1460
|
+
modifiedLine = modifiedLine.replace(/\{([^{}]*)\}/g, (match, content) => {
|
|
1461
|
+
// Skip if already properly quoted
|
|
1462
|
+
if (content.trim().startsWith('"') && content.trim().endsWith('"')) {
|
|
1463
|
+
return match;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// Check if content needs quoting (contains problematic patterns)
|
|
1467
|
+
if (needsQuoting(content)) {
|
|
1468
|
+
wasFixed = true;
|
|
1469
|
+
// Replace internal double quotes with single quotes to avoid nesting
|
|
1470
|
+
const safeContent = content.replace(/"/g, "'");
|
|
1471
|
+
return `{"${safeContent}"}`;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
return match;
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
return modifiedLine;
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
if (wasFixed) {
|
|
1482
|
+
const fixedContent = fixedLines.join('\n');
|
|
1483
|
+
|
|
1484
|
+
// Validate the fixed content
|
|
1485
|
+
try {
|
|
1486
|
+
const quickValidation = await validateMermaidDiagram(fixedContent);
|
|
1487
|
+
if (quickValidation.isValid) {
|
|
1488
|
+
// Node label auto-fix worked!
|
|
1489
|
+
const originalDiagram = postSubgraphDiagrams[invalidDiagram.originalIndex];
|
|
1490
|
+
const attributesStr = originalDiagram.attributes ? ` ${originalDiagram.attributes}` : '';
|
|
1491
|
+
const newCodeBlock = `\`\`\`mermaid${attributesStr}\n${fixedContent}\n\`\`\``;
|
|
1492
|
+
|
|
1493
|
+
fixedResponse = fixedResponse.slice(0, originalDiagram.startIndex) +
|
|
1494
|
+
newCodeBlock +
|
|
1495
|
+
fixedResponse.slice(originalDiagram.endIndex);
|
|
1496
|
+
|
|
1497
|
+
fixingResults.push({
|
|
1498
|
+
originalIndex: invalidDiagram.originalIndex,
|
|
1499
|
+
wasFixed: true,
|
|
1500
|
+
originalError: invalidDiagram.error,
|
|
1501
|
+
fixMethod: 'node_label_quote_wrapping',
|
|
1502
|
+
fixedWithNodeLabelQuoting: true
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
nodeLabelFixesApplied = true;
|
|
1506
|
+
|
|
1507
|
+
if (debug) {
|
|
1508
|
+
console.log(`[DEBUG] Mermaid validation: Fixed diagram ${invalidDiagram.originalIndex + 1} with node label quote wrapping`);
|
|
1509
|
+
console.log(`[DEBUG] Mermaid validation: Original error: ${invalidDiagram.error}`);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
if (debug) {
|
|
1514
|
+
console.log(`[DEBUG] Mermaid validation: Node label auto-fix didn't work for diagram ${invalidDiagram.originalIndex + 1}: ${error.message}`);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// If node label fixes were applied, re-validate the entire response
|
|
1522
|
+
if (nodeLabelFixesApplied) {
|
|
1523
|
+
const revalidation = await validateMermaidResponse(fixedResponse);
|
|
1524
|
+
if (revalidation.isValid) {
|
|
1525
|
+
// All diagrams are now valid, return without AI fixing
|
|
1526
|
+
const totalTime = Date.now() - startTime;
|
|
1527
|
+
if (debug) {
|
|
1528
|
+
console.log(`[DEBUG] Mermaid validation: All diagrams fixed with auto-fixes in ${totalTime}ms, no AI needed`);
|
|
1529
|
+
console.log(`[DEBUG] Mermaid validation: Applied ${fixingResults.length} auto-fixes (HTML entities + subgraph quotes + node label quotes)`);
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// Record auto-fix success in telemetry
|
|
1533
|
+
if (tracer) {
|
|
1534
|
+
tracer.recordMermaidValidationEvent('auto_fix_completed', {
|
|
1535
|
+
'mermaid_validation.success': true,
|
|
1536
|
+
'mermaid_validation.fix_method': 'auto_fixes',
|
|
1537
|
+
'mermaid_validation.diagrams_fixed': fixingResults.length,
|
|
1538
|
+
'mermaid_validation.duration_ms': totalTime
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
return {
|
|
1542
|
+
...revalidation,
|
|
1543
|
+
wasFixed: true,
|
|
1544
|
+
originalResponse: response,
|
|
1545
|
+
fixedResponse: fixedResponse,
|
|
1546
|
+
fixingResults: fixingResults,
|
|
1547
|
+
performanceMetrics: {
|
|
1548
|
+
totalTimeMs: totalTime,
|
|
1549
|
+
aiFixingTimeMs: 0,
|
|
1550
|
+
finalValidationTimeMs: 0,
|
|
1551
|
+
diagramsProcessed: fixingResults.length,
|
|
1552
|
+
diagramsFixed: fixingResults.length
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// Re-extract diagrams and re-validate after HTML entity fixes
|
|
1559
|
+
const { diagrams: updatedDiagrams } = extractMermaidFromMarkdown(fixedResponse);
|
|
1560
|
+
const updatedValidation = await validateMermaidResponse(fixedResponse);
|
|
1561
|
+
|
|
1562
|
+
// Still have invalid diagrams after all auto-fixes, proceed with AI fixing
|
|
1563
|
+
if (debug) {
|
|
1564
|
+
const stillInvalidAfterHtml = updatedValidation?.diagrams?.filter(d => !d.isValid)?.length || invalidCount;
|
|
1565
|
+
console.log(`[DEBUG] Mermaid validation: ${stillInvalidAfterHtml} diagrams still invalid after HTML entity decoding, starting AI fixing...`);
|
|
1566
|
+
console.log(`[DEBUG] Mermaid validation: HTML entity fixes applied: ${fixingResults.length}`);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// Create specialized fixing agent for remaining invalid diagrams
|
|
1570
|
+
if (debug) {
|
|
1571
|
+
console.log(`[DEBUG] Mermaid validation: Creating specialized AI fixing agent...`);
|
|
1572
|
+
}
|
|
1573
|
+
const aiFixingStart = Date.now();
|
|
1574
|
+
const mermaidFixer = new MermaidFixingAgent({
|
|
1575
|
+
path, provider, model, debug, tracer
|
|
1576
|
+
});
|
|
1577
|
+
|
|
1578
|
+
const stillInvalidDiagrams = updatedValidation.diagrams
|
|
1579
|
+
.map((result, index) => ({ ...result, originalIndex: index }))
|
|
1580
|
+
.filter(result => !result.isValid)
|
|
1581
|
+
.reverse();
|
|
1582
|
+
|
|
1583
|
+
if (debug) {
|
|
1584
|
+
console.log(`[DEBUG] Mermaid validation: Found ${stillInvalidDiagrams.length} diagrams requiring AI fixing`);
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
for (const invalidDiagram of stillInvalidDiagrams) {
|
|
1588
|
+
if (debug) {
|
|
1589
|
+
console.log(`[DEBUG] Mermaid validation: Attempting AI fix for diagram ${invalidDiagram.originalIndex + 1}`);
|
|
1590
|
+
console.log(`[DEBUG] Mermaid validation: Diagram type: ${invalidDiagram.diagramType || 'unknown'}`);
|
|
1591
|
+
console.log(`[DEBUG] Mermaid validation: Error to fix: ${invalidDiagram.error}`);
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
const diagramFixStart = Date.now();
|
|
1595
|
+
try {
|
|
1596
|
+
const fixedContent = await mermaidFixer.fixMermaidDiagram(
|
|
1597
|
+
invalidDiagram.content,
|
|
1598
|
+
[invalidDiagram.error],
|
|
1599
|
+
{ diagramType: invalidDiagram.diagramType }
|
|
1600
|
+
);
|
|
1601
|
+
const diagramFixTime = Date.now() - diagramFixStart;
|
|
1602
|
+
|
|
1603
|
+
if (fixedContent && fixedContent !== invalidDiagram.content) {
|
|
1604
|
+
// Replace the diagram in the response
|
|
1605
|
+
const originalDiagram = updatedDiagrams[invalidDiagram.originalIndex];
|
|
1606
|
+
const attributesStr = originalDiagram.attributes ? ` ${originalDiagram.attributes}` : '';
|
|
1607
|
+
const newCodeBlock = `\`\`\`mermaid${attributesStr}\n${fixedContent}\n\`\`\``;
|
|
1608
|
+
|
|
1609
|
+
fixedResponse = fixedResponse.slice(0, originalDiagram.startIndex) +
|
|
1610
|
+
newCodeBlock +
|
|
1611
|
+
fixedResponse.slice(originalDiagram.endIndex);
|
|
1612
|
+
|
|
1613
|
+
fixingResults.push({
|
|
1614
|
+
diagramIndex: invalidDiagram.originalIndex,
|
|
1615
|
+
wasFixed: true,
|
|
1616
|
+
originalContent: invalidDiagram.content,
|
|
1617
|
+
fixedContent: fixedContent,
|
|
1618
|
+
originalError: invalidDiagram.error,
|
|
1619
|
+
aiFixingTimeMs: diagramFixTime
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
if (debug) {
|
|
1623
|
+
console.log(`[DEBUG] Mermaid validation: Successfully fixed diagram ${invalidDiagram.originalIndex + 1} in ${diagramFixTime}ms`);
|
|
1624
|
+
console.log(`[DEBUG] Mermaid validation: Original error: ${invalidDiagram.error}`);
|
|
1625
|
+
console.log(`[DEBUG] Mermaid validation: Content changes: ${invalidDiagram.content.length} -> ${fixedContent.length} chars`);
|
|
1626
|
+
}
|
|
1627
|
+
} else {
|
|
1628
|
+
fixingResults.push({
|
|
1629
|
+
diagramIndex: invalidDiagram.originalIndex,
|
|
1630
|
+
wasFixed: false,
|
|
1631
|
+
originalContent: invalidDiagram.content,
|
|
1632
|
+
originalError: invalidDiagram.error,
|
|
1633
|
+
fixingError: 'No valid fix generated',
|
|
1634
|
+
aiFixingTimeMs: diagramFixTime
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1637
|
+
if (debug) {
|
|
1638
|
+
console.log(`[DEBUG] Mermaid validation: AI fix failed for diagram ${invalidDiagram.originalIndex + 1} - no valid fix generated`);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
} catch (error) {
|
|
1642
|
+
const diagramFixTime = Date.now() - diagramFixStart;
|
|
1643
|
+
fixingResults.push({
|
|
1644
|
+
diagramIndex: invalidDiagram.originalIndex,
|
|
1645
|
+
wasFixed: false,
|
|
1646
|
+
originalContent: invalidDiagram.content,
|
|
1647
|
+
originalError: invalidDiagram.error,
|
|
1648
|
+
fixingError: error.message,
|
|
1649
|
+
aiFixingTimeMs: diagramFixTime
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
if (debug) {
|
|
1653
|
+
console.log(`[DEBUG] Mermaid validation: AI fix failed for diagram ${invalidDiagram.originalIndex + 1} after ${diagramFixTime}ms: ${error.message}`);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
// Re-validate the fixed response
|
|
1659
|
+
const finalValidationStart = Date.now();
|
|
1660
|
+
const finalValidation = await validateMermaidResponse(fixedResponse);
|
|
1661
|
+
const finalValidationTime = Date.now() - finalValidationStart;
|
|
1662
|
+
const totalTime = Date.now() - startTime;
|
|
1663
|
+
const aiFixingTime = Date.now() - aiFixingStart;
|
|
1664
|
+
|
|
1665
|
+
// Check if any diagrams were actually fixed
|
|
1666
|
+
const wasActuallyFixed = fixingResults.some(result => result.wasFixed);
|
|
1667
|
+
const fixedCount = fixingResults.filter(result => result.wasFixed).length;
|
|
1668
|
+
const totalAttempts = fixingResults.length;
|
|
1669
|
+
|
|
1670
|
+
if (debug) {
|
|
1671
|
+
console.log(`[DEBUG] Mermaid validation: Final validation completed in ${finalValidationTime}ms`);
|
|
1672
|
+
console.log(`[DEBUG] Mermaid validation: Total process time: ${totalTime}ms (AI fixing: ${aiFixingTime}ms)`);
|
|
1673
|
+
console.log(`[DEBUG] Mermaid validation: Fixed ${fixedCount}/${totalAttempts} diagrams with AI`);
|
|
1674
|
+
console.log(`[DEBUG] Mermaid validation: Final result - all valid: ${finalValidation.isValid}`);
|
|
1675
|
+
|
|
1676
|
+
if (mermaidFixer.getTokenUsage) {
|
|
1677
|
+
const tokenUsage = mermaidFixer.getTokenUsage();
|
|
1678
|
+
console.log(`[DEBUG] Mermaid validation: AI token usage - prompt: ${tokenUsage?.promptTokens || 0}, completion: ${tokenUsage?.completionTokens || 0}`);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// Record final completion in telemetry
|
|
1683
|
+
if (tracer) {
|
|
1684
|
+
tracer.recordMermaidValidationEvent('completed', {
|
|
1685
|
+
'mermaid_validation.success': finalValidation.isValid,
|
|
1686
|
+
'mermaid_validation.was_fixed': wasActuallyFixed,
|
|
1687
|
+
'mermaid_validation.diagrams_processed': totalAttempts,
|
|
1688
|
+
'mermaid_validation.diagrams_fixed': fixedCount,
|
|
1689
|
+
'mermaid_validation.total_duration_ms': totalTime,
|
|
1690
|
+
'mermaid_validation.ai_fixing_duration_ms': aiFixingTime,
|
|
1691
|
+
'mermaid_validation.final_validation_duration_ms': finalValidationTime,
|
|
1692
|
+
'mermaid_validation.token_usage': mermaidFixer.getTokenUsage ? mermaidFixer.getTokenUsage() : null
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
return {
|
|
1697
|
+
...finalValidation,
|
|
1698
|
+
wasFixed: wasActuallyFixed,
|
|
1699
|
+
originalResponse: response,
|
|
1700
|
+
fixedResponse: fixedResponse,
|
|
1701
|
+
fixingResults: fixingResults,
|
|
1702
|
+
performanceMetrics: {
|
|
1703
|
+
totalTimeMs: totalTime,
|
|
1704
|
+
aiFixingTimeMs: aiFixingTime,
|
|
1705
|
+
finalValidationTimeMs: finalValidationTime,
|
|
1706
|
+
diagramsProcessed: totalAttempts,
|
|
1707
|
+
diagramsFixed: fixedCount
|
|
1708
|
+
},
|
|
1709
|
+
tokenUsage: mermaidFixer.getTokenUsage()
|
|
1710
|
+
};
|
|
1711
|
+
|
|
1712
|
+
} catch (error) {
|
|
1713
|
+
if (debug) {
|
|
1714
|
+
console.error(`[DEBUG] Mermaid fixing agent failed: ${error.message}`);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// Return original validation with fixing error
|
|
1718
|
+
return {
|
|
1719
|
+
...validation,
|
|
1720
|
+
wasFixed: false,
|
|
1721
|
+
originalResponse: response,
|
|
1722
|
+
fixedResponse: response,
|
|
1723
|
+
fixingError: error.message
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
}
|