/** * Reasoning Trace Parser * Handles parsing and formatting of model reasoning traces from OCR outputs */ class ReasoningParser { /** * Detect if text contains reasoning trace markers * @param {string} text - The text to check * @returns {boolean} - True if reasoning trace is detected */ static detectReasoningTrace(text) { if (!text || typeof text !== 'string') return false; // Check for complete reasoning trace patterns (both opening and closing tags) const completePatterns = [ { start: //i, end: /<\/think>/i }, { start: //i, end: /<\/thinking>/i }, { start: //i, end: /<\/reasoning>/i }, { start: //i, end: /<\/thought>/i } ]; // Only return true if we find BOTH opening and closing tags return completePatterns.some(pattern => pattern.start.test(text) && pattern.end.test(text) ); } /** * Parse reasoning content from text * @param {string} text - The text containing reasoning trace * @returns {object} - Object with reasoning and answer sections */ static parseReasoningContent(text) { if (!text) { return { reasoning: null, answer: null, original: text }; } // Try multiple patterns for flexibility const patterns = [ { start: //i, end: /<\/think>/i, answerStart: //i, answerEnd: /<\/answer>/i }, { start: //i, end: /<\/thinking>/i, answerStart: //i, answerEnd: /<\/answer>/i }, { start: //i, end: /<\/reasoning>/i, answerStart: //i, answerEnd: /<\/output>/i } ]; for (const pattern of patterns) { const reasoningMatch = text.match(new RegExp( pattern.start.source + '([\\s\\S]*?)' + pattern.end.source, 'i' )); const answerMatch = text.match(new RegExp( pattern.answerStart.source + '([\\s\\S]*?)' + pattern.answerEnd.source, 'i' )); if (reasoningMatch || answerMatch) { return { reasoning: reasoningMatch ? reasoningMatch[1].trim() : null, answer: answerMatch ? answerMatch[1].trim() : null, hasReasoning: !!reasoningMatch, hasAnswer: !!answerMatch, original: text }; } } // Check if there are incomplete reasoning tags (opening but no closing) const hasOpeningTag = /|||/i.test(text); if (hasOpeningTag) { console.warn('Incomplete reasoning trace detected - missing closing tags'); } // If no patterns match, return original text as answer return { reasoning: null, answer: text, hasReasoning: false, hasAnswer: true, original: text }; } /** * Format reasoning steps for display * @param {string} reasoningText - The raw reasoning text * @returns {object} - Formatted reasoning with steps and metadata */ static formatReasoningSteps(reasoningText) { if (!reasoningText) return null; // Parse numbered steps (e.g., "1. Step content") const stepPattern = /^\d+\.\s+\*\*(.+?)\*\*(.+?)(?=^\d+\.\s|\z)/gms; const steps = []; let match; while ((match = stepPattern.exec(reasoningText)) !== null) { steps.push({ title: match[1].trim(), content: match[2].trim() }); } // If no numbered steps found, try to parse by line breaks if (steps.length === 0) { const lines = reasoningText.split('\n').filter(line => line.trim()); lines.forEach((line, index) => { // Check if line starts with a number const numberedMatch = line.match(/^(\d+)\.\s*(.+)/); if (numberedMatch) { const title = numberedMatch[2].replace(/\*\*/g, '').trim(); steps.push({ number: numberedMatch[1], title: title, content: '' }); } else if (steps.length > 0) { // Add to previous step's content steps[steps.length - 1].content += '\n' + line; } }); } return { steps: steps, rawText: reasoningText, stepCount: steps.length, characterCount: reasoningText.length, wordCount: reasoningText.split(/\s+/).filter(w => w).length }; } /** * Extract key insights from reasoning * @param {string} reasoningText - The reasoning text * @returns {array} - Array of key insights or decisions */ static extractInsights(reasoningText) { if (!reasoningText) return []; const insights = []; // Look for decision points and key observations const patterns = [ /decision:\s*(.+)/gi, /observation:\s*(.+)/gi, /note:\s*(.+)/gi, /important:\s*(.+)/gi, /key finding:\s*(.+)/gi ]; patterns.forEach(pattern => { let match; while ((match = pattern.exec(reasoningText)) !== null) { insights.push(match[1].trim()); } }); return insights; } /** * Get summary statistics about the reasoning trace * @param {object} parsedContent - Parsed reasoning content * @returns {object} - Statistics about the reasoning */ static getReasoningStats(parsedContent) { if (!parsedContent || !parsedContent.reasoning) { return { hasReasoning: false, reasoningLength: 0, answerLength: 0, reasoningRatio: 0 }; } const reasoningLength = parsedContent.reasoning.length; const answerLength = parsedContent.answer ? parsedContent.answer.length : 0; const totalLength = reasoningLength + answerLength; return { hasReasoning: true, reasoningLength: reasoningLength, answerLength: answerLength, totalLength: totalLength, reasoningRatio: totalLength > 0 ? (reasoningLength / totalLength * 100).toFixed(1) : 0, reasoningWords: parsedContent.reasoning.split(/\s+/).filter(w => w).length, answerWords: parsedContent.answer ? parsedContent.answer.split(/\s+/).filter(w => w).length : 0 }; } /** * Format reasoning for export * @param {object} parsedContent - Parsed reasoning content * @param {boolean} includeReasoning - Whether to include reasoning in export * @returns {string} - Formatted text for export */ static formatForExport(parsedContent, includeReasoning = true) { if (!parsedContent) return ''; let exportText = ''; if (includeReasoning && parsedContent.reasoning) { exportText += '=== MODEL REASONING ===\n\n'; exportText += parsedContent.reasoning; exportText += '\n\n=== FINAL OUTPUT ===\n\n'; } if (parsedContent.answer) { exportText += parsedContent.answer; } return exportText; } } // Export for use in other scripts window.ReasoningParser = ReasoningParser;