@gork-labs/secondbrain-mcp
Version:
Second Brain MCP Server - Agent team orchestration with dynamic tool discovery
220 lines (219 loc) • 8.62 kB
JavaScript
/**
* Forgiving JSON Parser for AI-generated content
* Handles common JSON syntax errors from AI model responses
*/
import { logger } from './logger.js';
/**
* Attempts to parse JSON with various fallback strategies for common AI errors
*/
export function parseJsonForgiving(jsonString) {
const fixesApplied = [];
let initialError = null;
try {
// First, try standard JSON.parse
const data = JSON.parse(jsonString);
return { success: true, data };
}
catch (error) {
initialError = error instanceof Error ? error : new Error(String(error));
logger.debug('Standard JSON parse failed, attempting forgiving parse', {
error: error instanceof Error ? error.message : String(error),
contentPreview: jsonString.substring(0, 200) + '...'
});
}
// Apply various fixes progressively
let cleanedJson = jsonString;
try {
// Strategy 1: Extract JSON from code blocks or surrounding text
const jsonMatch = cleanedJson.match(/\{[\s\S]*\}/);
if (jsonMatch && jsonMatch[0] !== cleanedJson) {
cleanedJson = jsonMatch[0];
fixesApplied.push('Extracted JSON from surrounding text');
}
// Strategy 2: Remove trailing commas
cleanedJson = cleanedJson.replace(/,(\s*[}\]])/g, '$1');
if (cleanedJson !== jsonString) {
fixesApplied.push('Removed trailing commas');
}
// Strategy 3: Fix unquoted property names
cleanedJson = cleanedJson.replace(/([{,]\s*)(\w+)(\s*:)/g, '$1"$2"$3');
if (fixesApplied.length === 0 || cleanedJson !== jsonString) {
fixesApplied.push('Added quotes to property names');
}
// Strategy 4: Fix single quotes to double quotes
const beforeQuoteFix = cleanedJson;
cleanedJson = cleanedJson.replace(/'/g, '"');
if (cleanedJson !== beforeQuoteFix) {
fixesApplied.push('Converted single quotes to double quotes');
}
// Strategy 5: Try to fix incomplete JSON objects
if (!cleanedJson.trim().endsWith('}') && !cleanedJson.trim().endsWith(']')) {
const openBraces = (cleanedJson.match(/\{/g) || []).length;
const closeBraces = (cleanedJson.match(/\}/g) || []).length;
if (openBraces > closeBraces) {
cleanedJson += '}'.repeat(openBraces - closeBraces);
fixesApplied.push('Added missing closing braces');
}
const openBrackets = (cleanedJson.match(/\[/g) || []).length;
const closeBrackets = (cleanedJson.match(/\]/g) || []).length;
if (openBrackets > closeBrackets) {
cleanedJson += ']'.repeat(openBrackets - closeBrackets);
fixesApplied.push('Added missing closing brackets');
}
}
// Strategy 6: Handle multiline strings that break JSON
cleanedJson = cleanedJson.replace(/:\s*"([^"]*)\n([^"]*)"/, (match, p1, p2) => {
return `: "${p1}\\n${p2}"`;
});
// Try parsing the cleaned JSON
const data = JSON.parse(cleanedJson);
logger.info('Successfully parsed JSON with fixes', {
fixesApplied,
originalLength: jsonString.length,
cleanedLength: cleanedJson.length
});
return { success: true, data, fixesApplied };
}
catch (finalError) {
// Strategy 7: Try to parse as individual properties if it's an object
try {
const partialData = attemptPartialParse(cleanedJson);
if (partialData) {
fixesApplied.push('Partial property extraction');
return { success: true, data: partialData, fixesApplied };
}
}
catch (partialError) {
// Ignore partial parse errors
}
// Strategy 8: Return a fallback object with extracted content
const fallbackData = createFallbackObject(jsonString);
if (fallbackData) {
fixesApplied.push('Created fallback object from content');
return { success: true, data: fallbackData, fixesApplied };
}
logger.error('All JSON parsing strategies failed', {
originalError: initialError instanceof Error ? initialError.message : String(initialError),
finalError: finalError instanceof Error ? finalError.message : String(finalError),
fixesAttempted: fixesApplied,
contentPreview: jsonString.substring(0, 300) + '...'
});
return {
success: false,
error: finalError instanceof Error ? finalError.message : String(finalError),
fixesApplied
};
}
}
/**
* Attempts to parse individual properties from malformed JSON
*/
function attemptPartialParse(jsonString) {
const result = {};
// Try to extract key-value pairs using regex
const propertyRegex = /"([^"]+)"\s*:\s*("(?:[^"\\]|\\.)*"|[^,}\]]+)/g;
let match;
while ((match = propertyRegex.exec(jsonString)) !== null) {
const [, key, value] = match;
try {
// Try to parse the value
if (value.startsWith('"') && value.endsWith('"')) {
result[key] = value.slice(1, -1); // Remove quotes
}
else if (value === 'true' || value === 'false') {
result[key] = value === 'true';
}
else if (value === 'null') {
result[key] = null;
}
else if (!isNaN(Number(value))) {
result[key] = Number(value);
}
else if (value.startsWith('[') || value.startsWith('{')) {
result[key] = JSON.parse(value);
}
else {
result[key] = value.trim();
}
}
catch (parseError) {
// If value parsing fails, store as string
result[key] = value;
}
}
return Object.keys(result).length > 0 ? result : null;
}
/**
* Creates a fallback object with basic structure for validation
*/
function createFallbackObject(content) {
const fallback = {
deliverables: {},
memory_operations: [],
metadata: {
chatmode: 'unknown',
task_completion_status: 'partial',
processing_time: 'parsing_error',
confidence_level: 'low'
}
};
// Try to extract some basic information
try {
// Look for analysis content
const analysisMatch = content.match(/"analysis"\s*:\s*"([^"]+)"/);
if (analysisMatch) {
fallback.deliverables.analysis = analysisMatch[1];
}
// Look for recommendations
const recommendationsMatch = content.match(/"recommendations"\s*:\s*\[([^\]]+)\]/);
if (recommendationsMatch) {
try {
fallback.deliverables.recommendations = JSON.parse(`[${recommendationsMatch[1]}]`);
}
catch {
fallback.deliverables.recommendations = [recommendationsMatch[1]];
}
}
// Look for chatmode
const chatmodeMatch = content.match(/"chatmode"\s*:\s*"([^"]+)"/);
if (chatmodeMatch) {
fallback.metadata.chatmode = chatmodeMatch[1];
}
}
catch (extractError) {
logger.warn('Content extraction failed for fallback object', {
error: extractError instanceof Error ? extractError.message : String(extractError)
});
}
return fallback;
}
/**
* Validates that the parsed JSON has the basic structure expected for sub-agent responses
*/
export function validateSubAgentStructure(data) {
const issues = [];
if (!data || typeof data !== 'object') {
issues.push('Data is not an object');
return { valid: false, issues };
}
// Check for required top-level properties
if (!data.deliverables) {
issues.push('Missing deliverables property');
}
if (!data.metadata) {
issues.push('Missing metadata property');
}
if (data.memory_operations !== undefined && !Array.isArray(data.memory_operations)) {
issues.push('memory_operations is not an array');
}
// Check metadata structure
if (data.metadata) {
if (!data.metadata.chatmode) {
issues.push('Missing chatmode in metadata');
}
if (!data.metadata.task_completion_status) {
issues.push('Missing task_completion_status in metadata');
}
}
return { valid: issues.length === 0, issues };
}