|
|
|
|
|
|
|
|
|
|
|
class ReasoningParser { |
|
|
|
|
|
|
|
|
|
|
|
static detectReasoningTrace(text) { |
|
if (!text || typeof text !== 'string') return false; |
|
|
|
|
|
const completePatterns = [ |
|
{ start: /<think>/i, end: /<\/think>/i }, |
|
{ start: /<thinking>/i, end: /<\/thinking>/i }, |
|
{ start: /<reasoning>/i, end: /<\/reasoning>/i }, |
|
{ start: /<thought>/i, end: /<\/thought>/i } |
|
]; |
|
|
|
|
|
return completePatterns.some(pattern => |
|
pattern.start.test(text) && pattern.end.test(text) |
|
); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
static parseReasoningContent(text) { |
|
if (!text) { |
|
return { reasoning: null, answer: null, original: text }; |
|
} |
|
|
|
|
|
const patterns = [ |
|
{ |
|
start: /<think>/i, |
|
end: /<\/think>/i, |
|
answerStart: /<answer>/i, |
|
answerEnd: /<\/answer>/i |
|
}, |
|
{ |
|
start: /<thinking>/i, |
|
end: /<\/thinking>/i, |
|
answerStart: /<answer>/i, |
|
answerEnd: /<\/answer>/i |
|
}, |
|
{ |
|
start: /<reasoning>/i, |
|
end: /<\/reasoning>/i, |
|
answerStart: /<output>/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 |
|
}; |
|
} |
|
} |
|
|
|
|
|
const hasOpeningTag = /<think>|<thinking>|<reasoning>|<thought>/i.test(text); |
|
if (hasOpeningTag) { |
|
console.warn('Incomplete reasoning trace detected - missing closing tags'); |
|
} |
|
|
|
|
|
return { |
|
reasoning: null, |
|
answer: text, |
|
hasReasoning: false, |
|
hasAnswer: true, |
|
original: text |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
static formatReasoningSteps(reasoningText) { |
|
if (!reasoningText) return null; |
|
|
|
|
|
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 (steps.length === 0) { |
|
const lines = reasoningText.split('\n').filter(line => line.trim()); |
|
lines.forEach((line, index) => { |
|
|
|
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) { |
|
|
|
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 |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
static extractInsights(reasoningText) { |
|
if (!reasoningText) return []; |
|
|
|
const insights = []; |
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
} |
|
|
|
|
|
window.ReasoningParser = ReasoningParser; |