@gork-labs/secondbrain-mcp
Version:
Second Brain MCP Server - Agent team orchestration with dynamic tool discovery
180 lines (179 loc) • 7.67 kB
JavaScript
import { logger } from './logger.js';
/**
* Enhanced response parser with automatic retry and format correction
*/
export class ResponseParser {
static DEFAULT_MAX_RETRIES = 2;
/**
* Parse sub-agent response with retry mechanism
*/
static async parseWithRetry(responseText, chatmodeName, options = {}) {
const maxRetries = options.maxRetries ?? this.DEFAULT_MAX_RETRIES;
let lastError = null;
// Try parsing with automatic format correction first
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
let contentToParse = responseText;
// Apply format corrections on retries
if (attempt > 0) {
contentToParse = this.attemptFormatCorrection(responseText);
// If retry callback is provided, use it for intelligent retry
if (options.retryCallback && attempt === 1) {
try {
contentToParse = await options.retryCallback(responseText, attempt);
}
catch (callbackError) {
logger.warn('Retry callback failed, using format correction', {
chatmode: chatmodeName,
attempt,
error: callbackError instanceof Error ? callbackError.message : String(callbackError)
});
}
}
}
const result = this.parseContent(contentToParse, chatmodeName);
if (attempt > 0) {
logger.info('Successfully parsed after retry', {
chatmode: chatmodeName,
attempt,
originalLength: responseText.length,
correctedLength: contentToParse.length
});
}
return result;
}
catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt < maxRetries) {
logger.warn('Parse attempt failed, retrying', {
chatmode: chatmodeName,
attempt: attempt + 1,
maxRetries,
error: lastError.message,
contentPreview: responseText.substring(0, 150)
});
}
}
}
// All retries failed, create fallback response
logger.error('All parse attempts failed, using fallback', {
chatmode: chatmodeName,
attempts: maxRetries + 1,
finalError: lastError?.message,
contentLength: responseText.length
});
return this.createFallbackResponse(responseText, chatmodeName);
}
/**
* Parse content without retry (original behavior)
*/
static parseContent(responseText, chatmodeName) {
// Extract JSON from response text
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
throw new Error('No JSON found in sub-agent response');
}
const parsed = JSON.parse(jsonMatch[0]);
// Validate and ensure all required fields
return {
deliverables: {
documents: Array.isArray(parsed.deliverables?.documents)
? parsed.deliverables.documents
: undefined,
analysis: parsed.deliverables?.analysis || undefined,
recommendations: Array.isArray(parsed.deliverables?.recommendations)
? parsed.deliverables.recommendations
: undefined
},
memory_operations: Array.isArray(parsed.memory_operations)
? parsed.memory_operations
: [],
metadata: {
subagent: parsed.metadata?.subagent || chatmodeName,
task_completion_status: parsed.metadata?.task_completion_status || 'complete',
processing_time: parsed.metadata?.processing_time || 'unknown',
confidence_level: parsed.metadata?.confidence_level || 'medium'
}
};
}
/**
* Attempt automatic format correction on malformed responses
*/
static attemptFormatCorrection(content) {
let corrected = content;
// Remove common formatting issues
corrected = corrected.replace(/```json\s*/, '').replace(/```\s*$/, '');
corrected = corrected.replace(/^[^{]*/, ''); // Remove text before first {
// Find the last complete } to handle truncated responses
const lastBraceIndex = corrected.lastIndexOf('}');
if (lastBraceIndex !== -1) {
corrected = corrected.substring(0, lastBraceIndex + 1);
}
// Try to fix common JSON issues
corrected = corrected.replace(/,\s*}/g, '}'); // Remove trailing commas in objects
corrected = corrected.replace(/,\s*]/g, ']'); // Remove trailing commas in arrays
// Fix unescaped quotes in strings (basic attempt)
corrected = corrected.replace(/: "([^"]*)"([^",}\]]*)"([^",}\]]*)"/, ': "$1\\"$2\\"$3"');
// Ensure the JSON ends properly
if (!corrected.endsWith('}')) {
corrected += '}';
}
return corrected;
}
/**
* Create fallback response when all parsing attempts fail
*/
static createFallbackResponse(content, chatmodeName) {
return {
deliverables: {
analysis: this.extractAnalysisFromText(content),
recommendations: this.extractRecommendationsFromText(content),
documents: ['Parsing failed - raw content preserved']
},
memory_operations: [],
metadata: {
subagent: chatmodeName,
task_completion_status: content.toLowerCase().includes('error') ? 'failed' : 'partial',
processing_time: 'parsing_failed',
confidence_level: 'low'
}
};
}
/**
* Extract analysis-like content from plain text
*/
static extractAnalysisFromText(content) {
// Try to find analysis-like content
const lines = content.split('\n').filter(line => line.length > 30 &&
!line.startsWith('#') &&
!line.includes('```') &&
line.trim().length > 0);
if (lines.length === 0) {
// Return first 500 chars if no structured content found
return content.substring(0, 500) + (content.length > 500 ? '...' : '');
}
// Return first few substantial lines
return lines.slice(0, 3).join('\n');
}
/**
* Extract recommendations from plain text
*/
static extractRecommendationsFromText(content) {
const recommendations = [];
const lines = content.split('\n');
for (const line of lines) {
const trimmed = line.trim();
// Look for bullet points or numbered lists
if (trimmed.match(/^[-*]\s+/) || trimmed.match(/^\d+\.\s+/)) {
const rec = trimmed.replace(/^[-*\d.\s]+/, '').trim();
if (rec.length > 10) {
recommendations.push(rec);
}
}
}
if (recommendations.length === 0) {
return ['See analysis for details'];
}
return recommendations.slice(0, 5); // Limit to 5 recommendations
}
}