Spaces:
Sleeping
Sleeping
import { API_URL, API_TOKEN, API_USER } from '../constants'; | |
import { ApiInput, ApiResponseOutput, ProcessedResult, QaSectionResult, DetailedQaReport } from '../types'; | |
const MAX_RETRIES = 3; | |
const INITIAL_RETRY_DELAY_MS = 1000; | |
const RETRYABLE_STATUS_CODES = [429, 502, 503, 504]; // 429: Too Many Requests, 5xx: Server Errors | |
// Add jitter to delay to prevent thundering herd problem | |
const delay = (ms: number) => new Promise(res => setTimeout(res, ms + Math.random() * 500)); | |
// Structured error for workflow failures | |
class WorkflowError extends Error { | |
code: string; | |
at: 'network' | 'api' | 'stream' | 'parse' | 'unknown'; | |
debug?: string; | |
constructor(code: string, message: string, at: 'network' | 'api' | 'stream' | 'parse' | 'unknown' = 'unknown', debug?: string) { | |
super(message); | |
this.name = 'WorkflowError'; | |
this.code = code; | |
this.at = at; | |
this.debug = debug; | |
} | |
} | |
/** | |
* Removes <think>...</think> blocks from a string. | |
*/ | |
const cleanResponseText = (text: string): string => { | |
if (typeof text !== 'string') return ''; | |
return text.replace(/<think>[\s\S]*?<\/think>/g, '').trim(); | |
}; | |
/** | |
* Parses a single section of the QA report (e.g., TITLE, H1). | |
* This uses regular expressions to be robust against multiline content and format variations. | |
* @param sectionText The text content of a single QA section. | |
* @returns A structured object with the section's results. | |
*/ | |
const parseSection = (sectionText: string): QaSectionResult => { | |
console.log('Parsing section text:', sectionText.substring(0, 200)); | |
// ROBUST GRADE EXTRACTION - handles multiple formats | |
let grade = 'N/A'; | |
let gradeMatch = null; | |
// Try various grade patterns in order of specificity | |
const gradePatterns = [ | |
/-\s*\*\*Grade:\*\*\s*(.*)/, // - **Grade:** 100/100 | |
/•\s*\*\*Grade:\*\*\s*(.*)/, // • **Grade:** 100/100 | |
/\*\s*\*\*Grade:\*\*\s*(.*)/, // * **Grade:** 100/100 | |
/-\s*\*\*Grade\*\*:\s*(.*)/, // - **Grade**: 100/100 (colon without space) | |
/•\s*\*\*Grade\*\*:\s*(.*)/, // • **Grade**: 100/100 (colon without space) | |
/\*\s*\*\*Grade\*\*:\s*(.*)/, // * **Grade**: 100/100 | |
/(?:•|-|\*)\s*\*\*Grade\*\*:?:\s*(.*)/, // •/**/- **Grade**: 100/100 or - **Grade** 100/100 | |
/(?:•|-|\*)\s*Grade:?:\s*(.*)/, // • Grade: 100/100 or - Grade 100/100 | |
/Grade:?:\s*(\d+\/\d+|\d+)/m, // Grade: 100/100 (anywhere in text) | |
/(\d+\/\d+)\s*(?:grade|Grade)/ // 100/100 grade (reverse order) | |
]; | |
for (const pattern of gradePatterns) { | |
gradeMatch = sectionText.match(pattern); | |
if (gradeMatch) { | |
grade = gradeMatch[1].trim(); | |
break; | |
} | |
} | |
console.log('Grade match result:', gradeMatch, 'Final grade:', grade); | |
// ROBUST PASS EXTRACTION - handles multiple formats including the actual QA Guard format | |
let pass = false; | |
let passMatch = null; | |
// Try various pass patterns in order of specificity - FIXED TO HANDLE ACTUAL QA GUARD FORMAT | |
const passPatterns = [ | |
// First check for the actual QA Guard format: "### **TITLE: PASS** ✅" | |
/###\s*\*\*[^:]+:\s*(PASS|FAIL)\s*\*\*\s*(✅|❌)/i, | |
/###\s*\*\*[^:]+:\s*(PASS|FAIL)\s*\*\*/i, | |
/\*\*[^:]+:\s*(PASS|FAIL)\s*\*\*\s*(✅|❌)/i, | |
/\*\*[^:]+:\s*(PASS|FAIL)\s*\*\*/i, | |
// Then check for traditional patterns | |
/-\s*\*\*Pass:\*\*\s*(.*)/i, // - **Pass:** true | |
/•\s*\*\*Pass:\*\*\s*(.*)/i, // • **Pass:** true | |
/\*\s*\*\*Pass:\*\*\s*(.*)/i, // * **Pass:** true | |
/-\s*\*\*Pass\*\*:\s*(.*)/i, // - **Pass**: true (colon without space) | |
/•\s*\*\*Pass\*\*:\s*(.*)/i, // • **Pass**: true (colon without space) | |
/\*\s*\*\*Pass\*\*:\s*(.*)/i, // * **Pass**: true (colon without space) | |
/(?:•|-|\*)\s*\*\*Pass\*\*:?:\s*(.*)/i, // •/**/- **Pass**: true | |
/(?:•|-|\*)\s*Pass:?:\s*(.*)/i, // • Pass: true | |
/Pass:?:\s*(true|false|✅|❌|TRUE|FALSE)/im, // Pass: true (anywhere) | |
/(true|false|✅|❌|TRUE|FALSE)\s*pass/im // true pass (reverse) | |
]; | |
for (const pattern of passPatterns) { | |
passMatch = sectionText.match(pattern); | |
if (passMatch) { | |
const passValue = passMatch[1].toLowerCase().trim(); | |
pass = passValue.includes('pass') || | |
passValue.includes('true') || | |
passValue.includes('✅') || | |
passValue === 'yes' || | |
passValue === 'passed'; | |
break; | |
} | |
} | |
console.log('Pass match result:', passMatch, 'Final pass:', pass); | |
// ROBUST ERRORS EXTRACTION - handles multiple formats | |
let errors: string[] = ['No errors reported.']; | |
let errorsMatch = null; | |
// Try various error patterns | |
const errorPatterns = [ | |
/-\s*\*\*Errors:\*\*\s*([\s\S]*?)(?=\n-\s*\*\*|$)/, // - **Errors:** [] | |
/•\s*\*\*Errors:\*\*\s*([\s\S]*?)(?=\n•\s*\*\*|$)/, // • **Errors:** [] | |
/\*\s*\*\*Errors:\*\*\s*([\s\S]*?)(?=\n\*\s*\*\*|$)/, // * **Errors:** [] | |
/(?:•|-|\*)\s*\*\*Errors?\*\*:?:\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/, // Generic bullet/dash/star + bold | |
/(?:•|-|\*)\s*Errors:?:\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/, // Generic bullet/dash/star + no bold | |
/Errors:?:\s*([\s\S]*?)(?=\n(?:•|-|\*|\*\*)|$)/m // Errors: anywhere in text | |
]; | |
for (const pattern of errorPatterns) { | |
errorsMatch = sectionText.match(pattern); | |
if (errorsMatch) break; | |
} | |
if (errorsMatch) { | |
const errorsBlock = errorsMatch[1].trim(); | |
if (errorsBlock === '[]' || !errorsBlock || errorsBlock.toLowerCase() === 'none') { | |
errors = ['No errors reported.']; | |
} else if (errorsBlock.startsWith('[') && errorsBlock.includes(']')) { | |
// Handle array format: [] | |
try { | |
const parsed = JSON.parse(errorsBlock); | |
errors = Array.isArray(parsed) && parsed.length > 0 ? parsed : ['No errors reported.']; | |
} catch { | |
// If JSON parsing fails, treat as plain text | |
errors = [errorsBlock.replace(/[\[\]]/g, '').trim()]; | |
} | |
} else { | |
// Handle multi-line bullet format or plain text | |
const lines = errorsBlock.split('\n').map(e => e.trim().replace(/^[-•\*]\s*/, '')).filter(Boolean); | |
errors = lines.length > 0 ? lines : ['No errors reported.']; | |
} | |
} | |
// ENHANCED LOGIC: If we have a grade of 100/100 and no errors, but pass is still false, | |
// we should override the pass status based on the grade and errors | |
if (grade === '100/100' && (!errors || errors.length === 0 || errors[0] === 'No errors reported.')) { | |
console.log('Overriding pass status: Grade is 100/100 and no errors, setting pass to true'); | |
pass = true; | |
} | |
// Additional logic: If grade is high (80+) and no errors, likely a pass | |
if (grade !== 'N/A' && grade !== '0/100') { | |
const gradeNum = parseInt(grade.split('/')[0]); | |
if (gradeNum >= 80 && (!errors || errors.length === 0 || errors[0] === 'No errors reported.')) { | |
console.log(`Overriding pass status: Grade is ${grade} (${gradeNum}/100) and no errors, setting pass to true`); | |
pass = true; | |
} | |
} | |
// ROBUST ANALYSIS/CORRECTED CONTENT EXTRACTION - handles multiple formats | |
let corrected = 'Content analysis not available.'; | |
let contentMatch = null; | |
// Try various content patterns - Analysis, Corrected, or any descriptive text | |
const contentPatterns = [ | |
/-\s*\*\*Analysis:\*\*\s*([\s\S]*?)(?=\n-\s*\*\*|$)/, // - **Analysis:** text | |
/•\s*\*\*Analysis:\*\*\s*([\s\S]*?)(?=\n•\s*\*\*|$)/, // • **Analysis:** text | |
/\*\s*\*\*Analysis:\*\*\s*([\s\S]*?)(?=\n\*\s*\*\*|$)/, // * **Analysis:** text | |
/-\s*\*\*Corrected:\*\*\s*([\s\S]*?)(?=\n-\s*\*\*|$)/, // - **Corrected:** text | |
/•\s*\*\*Corrected:\*\*\s*([\s\S]*?)(?=\n•\s*\*\*|$)/, // • **Corrected:** text | |
/\*\s*\*\*Corrected:\*\*\s*([\s\S]*?)(?=\n\*\s*\*\*|$)/, // * **Corrected:** text | |
/(?:•|-|\*)\s*\*\*(?:Analysis|Corrected)\*\*:?:\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/, // Generic | |
/(?:•|-|\*)\s*(?:Analysis|Corrected):?:\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/, // No bold | |
/Analysis:?:\s*([\s\S]*?)(?=\n(?:•|-|\*|\*\*)|$)/m, // Analysis: anywhere | |
/Corrected:?:\s*([\s\S]*?)(?=\n(?:•|-|\*|\*\*)|$)/m // Corrected: anywhere | |
]; | |
for (const pattern of contentPatterns) { | |
contentMatch = sectionText.match(pattern); | |
if (contentMatch) { | |
corrected = contentMatch[1].trim(); | |
break; | |
} | |
} | |
// If no Analysis/Corrected found, extract the section title/content as fallback | |
if (!contentMatch || corrected.length < 10) { | |
// Extract title or first meaningful content line | |
const lines = sectionText.split('\n').map(l => l.trim()).filter(Boolean); | |
const titleLine = lines.find(line => !line.startsWith('•') && !line.startsWith('-') && !line.startsWith('*') && !line.includes('**') && line.length > 10); | |
if (titleLine) { | |
corrected = titleLine; | |
} | |
} | |
// Clean up any extra formatting | |
corrected = corrected.replace(/^#\s*/, '').replace(/###\s*\*\*[^*]+\*\*/, '').trim(); | |
console.log('Content match result:', contentMatch, 'Final corrected:', corrected.substring(0, 50)); | |
console.log('Final section result - Grade:', grade, 'Pass:', pass, 'Errors:', errors); | |
return { grade, pass, errors, corrected }; | |
}; | |
/** | |
* Parses the structured QA report format that comes as plain text with sections. | |
* @param qaText The raw structured QA text from the API. | |
* @returns An object containing the detailed parsed report and top-level pass/grade info. | |
*/ | |
const parseStructuredQaReport = (qaText: string): { detailedQaReport: DetailedQaReport, overallPass: boolean, overallGrade: string } => { | |
const defaultSection: QaSectionResult = { grade: 'N/A', pass: false, errors: ['Parsing failed'], corrected: '' }; | |
const defaultReport: DetailedQaReport = { | |
title: { ...defaultSection }, | |
meta: { ...defaultSection }, | |
h1: { ...defaultSection }, | |
copy: { ...defaultSection }, | |
overall: { grade: 'N/A', pass: false, primaryIssue: 'Parsing failed' } | |
}; | |
try { | |
// Split the text into sections by looking for section headers | |
const sections = qaText.split(/(?=^## [A-Z]+)/gm).filter(Boolean); | |
const parsedData: Partial<DetailedQaReport> = {}; | |
sections.forEach(sectionText => { | |
const lines = sectionText.trim().split('\n'); | |
const header = lines[0]?.replace('## ', '').trim().toLowerCase() || ''; | |
// Extract grade | |
const gradeLine = lines.find(line => line.includes('- Grade:'))?.trim() || ''; | |
const gradeMatch = gradeLine.match(/- Grade:\s*(\d+)\/100/) || gradeLine.match(/- Grade:\s*([^\n]+)/); | |
const grade = gradeMatch ? gradeMatch[1].trim() : 'N/A'; | |
// Extract pass status | |
const passLine = lines.find(line => line.includes('- Pass:'))?.trim() || ''; | |
const passMatch = passLine.match(/- Pass:\s*(true|false)/i); | |
const pass = passMatch ? passMatch[1].toLowerCase() === 'true' : false; | |
// Extract errors | |
let errors: string[] = []; | |
const errorsLineIndex = lines.findIndex(line => line.includes('- Errors:')); | |
if (errorsLineIndex !== -1) { | |
const errorsContent = lines[errorsLineIndex].replace('- Errors:', '').trim(); | |
if (errorsContent === '[]' || errorsContent === '') { | |
errors = ['No errors reported.']; | |
} else { | |
// Look for multi-line errors | |
let errorText = errorsContent; | |
for (let i = errorsLineIndex + 1; i < lines.length; i++) { | |
if (lines[i].startsWith('- ') && !lines[i].startsWith(' ')) break; | |
errorText += '\n' + lines[i].trim(); | |
} | |
// Parse error list | |
if (errorText.startsWith('[') && errorText.includes(']')) { | |
// Handle array format | |
try { | |
const parsedErrors = JSON.parse(errorText); | |
errors = Array.isArray(parsedErrors) ? parsedErrors : [errorText]; | |
} catch { | |
errors = [errorText]; | |
} | |
} else { | |
// Handle plain text or bullet list | |
errors = errorText.split('\n') | |
.map(e => e.trim().replace(/^- /, '')) | |
.filter(Boolean); | |
} | |
if (errors.length === 0) { | |
errors = ['No errors reported.']; | |
} | |
} | |
} else { | |
errors = ['Errors not found.']; | |
} | |
// Extract corrected content | |
let corrected = ''; | |
const correctedLineIndex = lines.findIndex(line => line.includes('- Corrected:')); | |
if (correctedLineIndex !== -1) { | |
corrected = lines.slice(correctedLineIndex) | |
.join('\n') | |
.replace('- Corrected:', '') | |
.trim(); | |
} else { | |
corrected = 'Correction not found.'; | |
} | |
const sectionResult: QaSectionResult = { grade, pass, errors, corrected }; | |
if (header.includes('title')) { | |
parsedData.title = sectionResult; | |
} else if (header.includes('meta')) { | |
parsedData.meta = sectionResult; | |
} else if (header.includes('h1')) { | |
parsedData.h1 = sectionResult; | |
} else if (header.includes('copy')) { | |
parsedData.copy = sectionResult; | |
} else if (header.includes('overall')) { | |
// Extract primary issue for overall section | |
const primaryIssueLine = lines.find(line => line.includes('- Primary Issue:'))?.trim() || ''; | |
const primaryIssue = primaryIssueLine.replace('- Primary Issue:', '').trim() || 'Not specified.'; | |
parsedData.overall = { grade, pass, primaryIssue }; | |
} | |
}); | |
const finalReport: DetailedQaReport = { | |
title: parsedData.title || { ...defaultSection, errors: ['Title section not found'] }, | |
meta: parsedData.meta || { ...defaultSection, errors: ['Meta section not found'] }, | |
h1: parsedData.h1 || { ...defaultSection, errors: ['H1 section not found'] }, | |
copy: parsedData.copy || { ...defaultSection, errors: ['Copy section not found'] }, | |
overall: parsedData.overall || { grade: 'N/A', pass: false, primaryIssue: 'Overall section not found' } | |
}; | |
return { | |
detailedQaReport: finalReport, | |
overallPass: finalReport.overall.pass, | |
overallGrade: finalReport.overall.grade | |
}; | |
} catch (error) { | |
console.error('Error parsing structured QA report:', error); | |
return { detailedQaReport: defaultReport, overallPass: false, overallGrade: 'N/A' }; | |
} | |
}; | |
/** | |
* Parses single-section format where all content is in one block. | |
* @param sectionText The section containing all embedded QA data. | |
* @param defaultReport Default report structure. | |
* @returns Parsed QA report data. | |
*/ | |
const parseSingleSectionFormat = (sectionText: string, defaultReport: DetailedQaReport): { detailedQaReport: DetailedQaReport, overallPass: boolean, overallGrade: string } => { | |
console.log('Parsing single-section format'); | |
// Extract embedded sections by looking for section patterns like "**TITLE:", "**META:", etc. | |
const titleMatch = sectionText.match(/\*\*TITLE[^*]*\*\*([\s\S]*?)(?=\*\*[A-Z]+|$)/i); | |
const metaMatch = sectionText.match(/\*\*META[^*]*\*\*([\s\S]*?)(?=\*\*[A-Z]+|$)/i); | |
const h1Match = sectionText.match(/\*\*H1[^*]*\*\*([\s\S]*?)(?=\*\*[A-Z]+|$)/i); | |
const copyMatch = sectionText.match(/\*\*COPY[^*]*\*\*([\s\S]*?)(?=\*\*[A-Z]+|$)/i); | |
const overallMatch = sectionText.match(/\*\*(?:OVERALL|ASSESSMENT)[^*]*\*\*([\s\S]*?)$/i); | |
const finalReport: DetailedQaReport = { | |
title: titleMatch ? parseSection(titleMatch[1]) : { ...defaultReport.title, errors: ['Title section not found'] }, | |
meta: metaMatch ? parseSection(metaMatch[1]) : { ...defaultReport.meta, errors: ['Meta section not found'] }, | |
h1: h1Match ? parseSection(h1Match[1]) : { ...defaultReport.h1, errors: ['H1 section not found'] }, | |
copy: copyMatch ? parseSection(copyMatch[1]) : { ...defaultReport.copy, errors: ['Copy section not found'] }, | |
overall: overallMatch ? { | |
grade: extractOverallGrade(overallMatch[1]), | |
pass: extractOverallPass(overallMatch[1]), | |
primaryIssue: 'Single-section format parsed' | |
} : { ...defaultReport.overall } | |
}; | |
return { | |
detailedQaReport: finalReport, | |
overallPass: finalReport.overall.pass, | |
overallGrade: finalReport.overall.grade | |
}; | |
}; | |
/** | |
* Helper function to extract overall grade from text. | |
*/ | |
const extractOverallGrade = (text: string): string => { | |
const gradeMatch = text.match(/Grade[^:]*:?\s*(\d+(?:\.\d+)?\/?\d*)/i) || text.match(/(\d+(?:\.\d+)?\/\d+)/); | |
return gradeMatch ? gradeMatch[1].trim() : 'N/A'; | |
}; | |
/** | |
* Helper function to extract overall pass from text. | |
*/ | |
const extractOverallPass = (text: string): boolean => { | |
const passMatch = text.match(/Pass[^:]*:?\s*(true|false|✅|❌|TRUE|FALSE)/i); | |
if (passMatch) { | |
const passValue = passMatch[1].toLowerCase().trim(); | |
return passValue.includes('true') || passValue.includes('✅'); | |
} | |
return false; | |
}; | |
/** | |
* Enhanced section parsing that captures ALL QA Guard content | |
*/ | |
const parseEnhancedSection = (sectionBlock: string): QaSectionResult => { | |
const baseSection = parseSection(sectionBlock); | |
// Extract detailed assessment content | |
const detailedAssessmentMatch = sectionBlock.match(/(?:•|-|\*)\s*\*\*(?:Detailed\s+)?Assessment\*\*?:?\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/i); | |
const explanationsMatch = sectionBlock.match(/(?:•|-|\*)\s*\*\*(?:Explanation|Reasoning)\*\*?:?\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/i); | |
// Extract key strengths | |
const keyStrengthsMatch = sectionBlock.match(/(?:•|-|\*)\s*\*\*Key\s+Strengths\*\*?:?\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/i); | |
const strengthsList = keyStrengthsMatch ? | |
keyStrengthsMatch[1].split('\n') | |
.map(line => line.replace(/^[-•*]\s*/, '').trim()) | |
.filter(line => line.length > 0) : undefined; | |
// Extract recommendations | |
const recommendationsMatch = sectionBlock.match(/(?:•|-|\*)\s*\*\*(?:Recommendations?|Suggestions?)\*\*?:?\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/i); | |
const recommendationsList = recommendationsMatch ? | |
recommendationsMatch[1].split('\n') | |
.map(line => line.replace(/^[-•*]\s*/, '').trim()) | |
.filter(line => line.length > 0) : undefined; | |
return { | |
...baseSection, | |
detailedAssessment: detailedAssessmentMatch ? detailedAssessmentMatch[1].trim() : undefined, | |
explanations: explanationsMatch ? explanationsMatch[1].trim() : undefined, | |
keyStrengths: strengthsList, | |
recommendations: recommendationsList, | |
rawContent: sectionBlock | |
}; | |
}; | |
/** | |
* Enhanced overall section parsing that captures ALL QA Guard content | |
*/ | |
const parseEnhancedOverallSection = (sectionBlock: string): { grade: string; pass: boolean; primaryIssue: string; detailedAssessment?: string; keyStrengths?: string[]; recommendations?: string[]; explanations?: string; rawContent?: string } => { | |
console.log('Parsing enhanced overall section with actual QA Guard format'); | |
console.log('Overall section preview:', sectionBlock.substring(0, 300)); | |
let grade = 'N/A'; | |
let pass = false; | |
let primaryIssue = 'Overall assessment not available.'; | |
let detailedAssessment = ''; | |
let keyStrengths: string[] = []; | |
let recommendations: string[] = []; | |
let explanations = ''; | |
// COMPREHENSIVE OVERALL GRADE PATTERN MATCHING | |
// Look for the actual QA Guard overall format: "Final Grade: 98.75/100" | |
const overallGradePatterns = [ | |
/Final\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Final Grade: 98.75/100 | |
/Overall\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Overall Grade: 98.75/100 | |
/Total\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Total Grade: 98.75/100 | |
/Combined\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Combined Grade: 98.75/100 | |
/Average\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Average Grade: 98.75/100 | |
/Mean\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Mean Grade: 98.75/100 | |
/Composite\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Composite Grade: 98.75/100 | |
/Final\s+Score:\s*(\d+(?:\.\d+)?)\/100/i, // Final Score: 98.75/100 | |
/Overall\s+Score:\s*(\d+(?:\.\d+)?)\/100/i, // Overall Score: 98.75/100 | |
/Total\s+Score:\s*(\d+(?:\.\d+)?)\/100/i, // Total Score: 98.75/100 | |
/Final\s+Rating:\s*(\d+(?:\.\d+)?)\/100/i, // Final Rating: 98.75/100 | |
/Overall\s+Rating:\s*(\d+(?:\.\d+)?)\/100/i, // Overall Rating: 98.75/100 | |
/Final\s+Mark:\s*(\d+(?:\.\d+)?)\/100/i, // Final Mark: 98.75/100 | |
/Overall\s+Mark:\s*(\d+(?:\.\d+)?)\/100/i, // Overall Mark: 98.75/100 | |
/Final\s+Points:\s*(\d+(?:\.\d+)?)\/100/i, // Final Points: 98.75/100 | |
/Overall\s+Points:\s*(\d+(?:\.\d+)?)\/100/i, // Overall Points: 98.75/100 | |
/-?\s*\*\*Final\s+Grade:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Final Grade:** 98.75/100 | |
/-?\s*\*\*Overall\s+Grade:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Overall Grade:** 98.75/100 | |
/-?\s*\*\*Total\s+Grade:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Total Grade:** 98.75/100 | |
/-?\s*\*\*Final\s+Score:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Final Score:** 98.75/100 | |
/-?\s*\*\*Overall\s+Score:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Overall Score:** 98.75/100 | |
/-?\s*\*\*Final\s+Rating:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Final Rating:** 98.75/100 | |
/-?\s*\*\*Overall\s+Rating:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Overall Rating:** 98.75/100 | |
/Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Grade: 98.75/100 | |
/Score:\s*(\d+(?:\.\d+)?)\/100/i, // Score: 98.75/100 | |
/Rating:\s*(\d+(?:\.\d+)?)\/100/i, // Rating: 98.75/100 | |
/Mark:\s*(\d+(?:\.\d+)?)\/100/i, // Mark: 98.75/100 | |
/Points:\s*(\d+(?:\.\d+)?)\/100/i, // Points: 98.75/100 | |
]; | |
let finalGradeMatch = null; | |
for (const pattern of overallGradePatterns) { | |
finalGradeMatch = sectionBlock.match(pattern); | |
if (finalGradeMatch) { | |
grade = `${finalGradeMatch[1]}/100`; | |
console.log('Found final grade:', grade); | |
break; | |
} | |
} | |
// COMPREHENSIVE OVERALL PASS STATUS PATTERN MATCHING | |
// Look for overall pass status: "Overall Pass: FALSE (due to META violation)" | |
const overallPassPatterns = [ | |
/Overall\s+Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Overall Pass: FALSE (due to META violation) | |
/Final\s+Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Final Pass: FALSE (due to META violation) | |
/Total\s+Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Total Pass: FALSE (due to META violation) | |
/Combined\s+Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Combined Pass: FALSE (due to META violation) | |
/Average\s+Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Average Pass: FALSE (due to META violation) | |
/Overall\s+Status:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Overall Status: FALSE (due to META violation) | |
/Final\s+Status:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Final Status: FALSE (due to META violation) | |
/Overall\s+Result:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Overall Result: FALSE (due to META violation) | |
/Final\s+Result:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Final Result: FALSE (due to META violation) | |
/Overall\s+Assessment:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Overall Assessment: FALSE (due to META violation) | |
/Final\s+Assessment:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Final Assessment: FALSE (due to META violation) | |
/-?\s*\*\*Overall\s+Pass:\*\*\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // - **Overall Pass:** FALSE (due to META violation) | |
/-?\s*\*\*Final\s+Pass:\*\*\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // - **Final Pass:** FALSE (due to META violation) | |
/-?\s*\*\*Total\s+Pass:\*\*\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // - **Total Pass:** FALSE (due to META violation) | |
/-?\s*\*\*Overall\s+Status:\*\*\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // - **Overall Status:** FALSE (due to META violation) | |
/-?\s*\*\*Final\s+Status:\*\*\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // - **Final Status:** FALSE (due to META violation) | |
/Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Pass: FALSE (due to META violation) | |
/Status:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Status: FALSE (due to META violation) | |
/Result:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Result: FALSE (due to META violation) | |
/Assessment:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Assessment: FALSE (due to META violation) | |
]; | |
let overallPassMatch = null; | |
for (const pattern of overallPassPatterns) { | |
overallPassMatch = sectionBlock.match(pattern); | |
if (overallPassMatch) { | |
pass = overallPassMatch[1].toUpperCase() === 'TRUE' || overallPassMatch[1].toUpperCase() === 'PASS'; | |
console.log('Found overall pass status:', pass); | |
break; | |
} | |
} | |
// COMPREHENSIVE PRIMARY ISSUE PATTERN MATCHING | |
// Look for primary issue in the pass status explanation | |
const issuePatterns = [ | |
/Overall\s+Pass:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Overall Pass: FALSE (due to META violation) | |
/Final\s+Pass:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Final Pass: FALSE (due to META violation) | |
/Total\s+Pass:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Total Pass: FALSE (due to META violation) | |
/Overall\s+Status:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Overall Status: FALSE (due to META violation) | |
/Final\s+Status:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Final Status: FALSE (due to META violation) | |
/Pass:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Pass: FALSE (due to META violation) | |
/Status:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Status: FALSE (due to META violation) | |
/Result:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Result: FALSE (due to META violation) | |
/Assessment:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Assessment: FALSE (due to META violation) | |
/-?\s*\*\*Overall\s+Pass:\*\*\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // - **Overall Pass:** FALSE (due to META violation) | |
/-?\s*\*\*Final\s+Pass:\*\*\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // - **Final Pass:** FALSE (due to META violation) | |
/-?\s*\*\*Total\s+Pass:\*\*\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // - **Total Pass:** FALSE (due to META violation) | |
/-?\s*\*\*Overall\s+Status:\*\*\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // - **Overall Status:** FALSE (due to META violation) | |
/-?\s*\*\*Final\s+Status:\*\*\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // - **Final Status:** FALSE (due to META violation) | |
/-?\s*\*\*Primary\s+Issue:\*\*\s*([^\n]+)/i, // - **Primary Issue:** Some sections have violations | |
/-?\s*\*\*Main\s+Issue:\*\*\s*([^\n]+)/i, // - **Main Issue:** Some sections have violations | |
/-?\s*\*\*Key\s+Issue:\*\*\s*([^\n]+)/i, // - **Key Issue:** Some sections have violations | |
/-?\s*\*\*Issue:\*\*\s*([^\n]+)/i, // - **Issue:** Some sections have violations | |
/-?\s*\*\*Problem:\*\*\s*([^\n]+)/i, // - **Problem:** Some sections have violations | |
/-?\s*\*\*Concern:\*\*\s*([^\n]+)/i, // - **Concern:** Some sections have violations | |
/Primary\s+Issue:\s*([^\n]+)/i, // Primary Issue: Some sections have violations | |
/Main\s+Issue:\s*([^\n]+)/i, // Main Issue: Some sections have violations | |
/Key\s+Issue:\s*([^\n]+)/i, // Key Issue: Some sections have violations | |
/Issue:\s*([^\n]+)/i, // Issue: Some sections have violations | |
/Problem:\s*([^\n]+)/i, // Problem: Some sections have violations | |
/Concern:\s*([^\n]+)/i, // Concern: Some sections have violations | |
]; | |
let issueMatch = null; | |
for (const pattern of issuePatterns) { | |
issueMatch = sectionBlock.match(pattern); | |
if (issueMatch) { | |
primaryIssue = issueMatch[1].trim(); | |
console.log('Found primary issue:', primaryIssue); | |
break; | |
} | |
} | |
// COMPREHENSIVE DETAILED BREAKDOWN PATTERN MATCHING | |
// Look for detailed breakdown sections | |
const breakdownPatterns = [ | |
/##\s+DETAILED\s+BREAKDOWN[^#]*/i, // ## DETAILED BREAKDOWN | |
/##\s+BREAKDOWN[^#]*/i, // ## BREAKDOWN | |
/##\s+ANALYSIS[^#]*/i, // ## ANALYSIS | |
/##\s+ASSESSMENT[^#]*/i, // ## ASSESSMENT | |
/##\s+EVALUATION[^#]*/i, // ## EVALUATION | |
/##\s+REVIEW[^#]*/i, // ## REVIEW | |
/##\s+SUMMARY[^#]*/i, // ## SUMMARY | |
/##\s+DETAILS[^#]*/i, // ## DETAILS | |
/##\s+EXPLANATION[^#]*/i, // ## EXPLANATION | |
/##\s+COMMENTS[^#]*/i, // ## COMMENTS | |
/##\s+NOTES[^#]*/i, // ## NOTES | |
/###\s+DETAILED\s+BREAKDOWN[^#]*/i, // ### DETAILED BREAKDOWN | |
/###\s+BREAKDOWN[^#]*/i, // ### BREAKDOWN | |
/###\s+ANALYSIS[^#]*/i, // ### ANALYSIS | |
/###\s+ASSESSMENT[^#]*/i, // ### ASSESSMENT | |
/###\s+EVALUATION[^#]*/i, // ### EVALUATION | |
/###\s+REVIEW[^#]*/i, // ### REVIEW | |
/###\s+SUMMARY[^#]*/i, // ### SUMMARY | |
/###\s+DETAILS[^#]*/i, // ### DETAILS | |
/###\s+EXPLANATION[^#]*/i, // ### EXPLANATION | |
/###\s+COMMENTS[^#]*/i, // ### COMMENTS | |
/###\s+NOTES[^#]*/i, // ### NOTES | |
]; | |
let breakdownMatch = null; | |
for (const pattern of breakdownPatterns) { | |
breakdownMatch = sectionBlock.match(pattern); | |
if (breakdownMatch) { | |
detailedAssessment = breakdownMatch[0]; | |
console.log('Found detailed breakdown'); | |
break; | |
} | |
} | |
// ENHANCED KEY STRENGTHS AND RECOMMENDATIONS EXTRACTION | |
// Extract key strengths and recommendations from the overall assessment | |
const lowerSection = sectionBlock.toLowerCase(); | |
// Extract key strengths | |
if (lowerSection.includes('compliance') && lowerSection.includes('requirements')) { | |
keyStrengths.push('Overall compliance with requirements'); | |
} | |
if (grade !== 'N/A' && parseFloat(grade.split('/')[0]) >= 80) { | |
keyStrengths.push('High overall grade achieved'); | |
} | |
if (lowerSection.includes('successful') || lowerSection.includes('approved')) { | |
keyStrengths.push('Overall assessment successful'); | |
} | |
if (lowerSection.includes('meets') && lowerSection.includes('standards')) { | |
keyStrengths.push('Meets overall standards'); | |
} | |
if (lowerSection.includes('satisfies') && lowerSection.includes('criteria')) { | |
keyStrengths.push('Satisfies overall criteria'); | |
} | |
if (lowerSection.includes('valid') || lowerSection.includes('correct')) { | |
keyStrengths.push('Overall content validation passed'); | |
} | |
// Extract recommendations | |
if (lowerSection.includes('violation') || lowerSection.includes('fail')) { | |
recommendations.push('Address identified violations'); | |
} | |
if (lowerSection.includes('correction') || lowerSection.includes('fix')) { | |
recommendations.push('Implement suggested corrections'); | |
} | |
if (lowerSection.includes('improve') || lowerSection.includes('enhance')) { | |
recommendations.push('Improve overall content quality'); | |
} | |
if (lowerSection.includes('adjust') || lowerSection.includes('modify')) { | |
recommendations.push('Adjust content to meet requirements'); | |
} | |
if (lowerSection.includes('review') && lowerSection.includes('carefully')) { | |
recommendations.push('Review content carefully'); | |
} | |
if (lowerSection.includes('consider') && lowerSection.includes('changes')) { | |
recommendations.push('Consider suggested changes'); | |
} | |
// FALLBACK PATTERN MATCHING | |
// If no grade found, try alternative patterns | |
if (grade === 'N/A') { | |
const fallbackGradePatterns = [ | |
/Grade:\s*(\d+(?:\.\d+)?)\/100/i, | |
/Score:\s*(\d+(?:\.\d+)?)\/100/i, | |
/Rating:\s*(\d+(?:\.\d+)?)\/100/i, | |
/Mark:\s*(\d+(?:\.\d+)?)\/100/i, | |
/Points:\s*(\d+(?:\.\d+)?)\/100/i, | |
/(\d+(?:\.\d+)?)\/100/i, | |
]; | |
for (const pattern of fallbackGradePatterns) { | |
const match = sectionBlock.match(pattern); | |
if (match) { | |
grade = `${match[1]}/100`; | |
console.log('Found fallback grade:', grade); | |
break; | |
} | |
} | |
} | |
// INFER PASS STATUS FROM GRADE IF NOT DETERMINED | |
if (grade !== 'N/A' && !overallPassMatch) { | |
const gradeNum = parseFloat(grade.split('/')[0]); | |
pass = gradeNum >= 80; | |
console.log('Inferred overall pass status from grade:', pass); | |
} | |
// FINAL PRIMARY ISSUE DETERMINATION | |
// If still no primary issue, generate one based on pass status | |
if (primaryIssue === 'Overall assessment not available.') { | |
if (pass) { | |
primaryIssue = 'All sections meet requirements'; | |
} else { | |
primaryIssue = 'Some sections have violations'; | |
} | |
} | |
console.log('Final overall result - Grade:', grade, 'Pass:', pass, 'Issue:', primaryIssue); | |
return { | |
grade, | |
pass, | |
primaryIssue, | |
detailedAssessment: detailedAssessment || undefined, | |
keyStrengths: keyStrengths.length > 0 ? keyStrengths : undefined, | |
recommendations: recommendations.length > 0 ? recommendations : undefined, | |
explanations: explanations || undefined, | |
rawContent: sectionBlock | |
}; | |
}; | |
/** | |
* Determine the type of an additional section based on its content | |
*/ | |
const determineSectionType = (sectionBlock: string): 'assessment' | 'strengths' | 'recommendations' | 'explanations' | 'other' => { | |
const lowerContent = sectionBlock.toLowerCase(); | |
if (lowerContent.includes('strength') || lowerContent.includes('positive') || lowerContent.includes('excellent')) { | |
return 'strengths'; | |
} else if (lowerContent.includes('recommend') || lowerContent.includes('suggest') || lowerContent.includes('improve')) { | |
return 'recommendations'; | |
} else if (lowerContent.includes('explain') || lowerContent.includes('reason') || lowerContent.includes('why')) { | |
return 'explanations'; | |
} else if (lowerContent.includes('assess') || lowerContent.includes('evaluate') || lowerContent.includes('analysis')) { | |
return 'assessment'; | |
} else { | |
return 'other'; | |
} | |
}; | |
/** | |
* Parses the new, structured QA report format. | |
* @param qaText The raw `qa_gaurd` string from the API. | |
* @returns An object containing the detailed parsed report and top-level pass/grade info. | |
*/ | |
const parseNewQaReport = (qaText: string): { detailedQaReport: DetailedQaReport, overallPass: boolean, overallGrade: string } => { | |
// Default structure in case of parsing failure | |
const defaultSection: QaSectionResult = { grade: 'N/A', pass: false, errors: ['Parsing failed'], corrected: '' }; | |
const defaultReport: DetailedQaReport = { | |
title: { ...defaultSection }, | |
meta: { ...defaultSection }, | |
h1: { ...defaultSection }, | |
copy: { ...defaultSection }, | |
overall: { grade: 'N/A', pass: false, primaryIssue: 'Parsing failed' }, | |
completeRawReport: qaText // Always preserve the complete raw report | |
}; | |
const cleanedQaText = cleanResponseText(qaText); | |
if (!cleanedQaText || typeof cleanedQaText !== 'string') { | |
return { detailedQaReport: defaultReport, overallPass: false, overallGrade: 'N/A' }; | |
} | |
console.log('Enhanced QA parsing - input text preview:', cleanedQaText.substring(0, 500)); | |
// COMPLETELY REWRITTEN TO HANDLE ACTUAL QA GUARD FORMAT | |
// The QA Guard uses formats like: "## **TITLE GRADE: 100/100 ✅ PASS**" | |
const parsedData: Partial<DetailedQaReport> = {}; | |
const additionalSections: { [sectionName: string]: { content: string; type: 'assessment' | 'strengths' | 'recommendations' | 'explanations' | 'other'; } } = {}; | |
// Enhanced section splitting to handle the actual QA Guard format | |
let sections: string[] = []; | |
// First, try to remove any leading overall evaluation header that might interfere with section splitting | |
const contentAfterOverallHeader = cleanedQaText.replace(/^(#\s*FINAL\s+QUALITY\s+ASSURANCE\s+EVALUATION|##\s*Section\s+Grades)\s*/i, '').trim(); | |
// Try multiple splitting strategies for robustness | |
// Order matters: more specific patterns should come first | |
const sectionHeaderPatterns = [ | |
// Highly specific: ### **SECTION NAME** - GRADE: X/100 ✅ PASS | |
/(?=(?:##|###)\s*\*\*([^*]+)\*\*\s*-\s*GRADE:)/g, | |
// Highly specific: ### **SECTION NAME: PASS** ✅ | |
/(?=(?:##|###)\s*\*\*([^*]+):\s*(?:PASS|FAIL)\*\*\s*(?:✅|❌))/g, | |
// Specific: ## **SECTION NAME GRADE:** | |
/(?=(?:##|###)\s*\*\*([^*]+)\s+GRADE:)/g, | |
// Specific: ## **SECTION NAME** (with optional pass/fail indicator) | |
/(?=(?:##|###)\s*\*\*([^*]+)\*\*(?:\s*(?:✅|❌|PASS|FAIL))?)/g, | |
// Generic: ## Section Name | |
/(?=(?:##|###)\s*[^#\n]*)/g, | |
]; | |
for (const pattern of sectionHeaderPatterns) { | |
sections = contentAfterOverallHeader.split(pattern).filter(Boolean); | |
if (sections.length > 1) { // If we found more than one section, this split was successful | |
console.log(`Using successful split pattern: ${pattern}`); | |
break; | |
} | |
} | |
// Fallback to paragraph splitting if no suitable header pattern was found | |
if (sections.length <= 1) { | |
sections = contentAfterOverallHeader.split(/\n\n+/).filter(section => section.trim().length > 20); | |
console.log('Using paragraph-based parsing as fallback (no header pattern matched)'); | |
} | |
// Ensure sections are not empty after splitting | |
sections = sections.filter(s => s.trim().length > 5); // Minimum length to be considered a valid section | |
console.log(`Found ${sections.length} sections to parse`); | |
sections.forEach((section, index) => { | |
console.log(`Section ${index} preview:`, section.substring(0, 150)); | |
}); | |
// Parse each section with enhanced logic | |
sections.forEach((sectionBlock, index) => { | |
const lines = sectionBlock.trim().split('\n'); | |
const headerRaw = lines[0]?.trim() || ''; | |
const header = headerRaw.toLowerCase(); | |
console.log(`Processing section ${index} with header:`, headerRaw); | |
let sectionType = ''; // Must be reset for each block | |
let sectionData: QaSectionResult | null = null; | |
// New, more robust section identification logic | |
const headerForTypeCheck = header | |
.replace(/^(#+\s*|\*\*)/, '') | |
.replace(/[:*]/g, '') | |
.replace(/\s*-\s*grade:.*/, '') // also strip grade info | |
.replace(/\s*(✅|❌|pass|fail).*/, '') // and status info | |
.trim(); | |
const words = headerForTypeCheck.split(/\s+/).filter(Boolean); | |
// If the normalized header is just ONE clean word, it's a primary section | |
if (words.length === 1) { | |
const typeWord = words[0]; | |
if (typeWord === 'title') sectionType = 'title'; | |
else if (typeWord === 'meta') sectionType = 'meta'; | |
else if (typeWord === 'h1') sectionType = 'h1'; | |
else if (typeWord === 'copy') sectionType = 'copy'; | |
else if (['overall', 'final', 'assessment'].includes(typeWord)) sectionType = 'overall'; | |
} | |
// If not identified, check for specific multi-word headers for 'overall' | |
if (!sectionType) { | |
if (headerForTypeCheck.startsWith('overall assessment') || headerForTypeCheck.startsWith('final assessment')) { | |
sectionType = 'overall'; | |
} | |
} | |
// Route to the correct parser or handle as an additional section | |
if (sectionType === 'title') { | |
console.log('Identified as TITLE section'); | |
sectionData = parseActualQAGuardSection(sectionBlock, sectionType); | |
if (sectionData) parsedData.title = sectionData; | |
} else if (sectionType === 'meta') { | |
console.log('Identified as META section'); | |
sectionData = parseActualQAGuardSection(sectionBlock, sectionType); | |
if (sectionData) parsedData.meta = sectionData; | |
} else if (sectionType === 'h1') { | |
console.log('Identified as H1 section'); | |
sectionData = parseActualQAGuardSection(sectionBlock, sectionType); | |
if (sectionData) parsedData.h1 = sectionData; | |
} else if (sectionType === 'copy') { | |
console.log('Identified as COPY section'); | |
sectionData = parseActualQAGuardSection(sectionBlock, sectionType); | |
if (sectionData) parsedData.copy = sectionData; | |
} else if (sectionType === 'overall') { | |
console.log('Identified as OVERALL section'); | |
const enhancedOverall = parseEnhancedOverallSection(sectionBlock); | |
parsedData.overall = enhancedOverall; | |
} else if (header.includes('grades by section') || header.includes('section grades')) { | |
console.log('Identified as introductory section, skipping.'); | |
return; | |
} else { | |
// Additional sections logic remains here | |
console.log('Identified as additional section'); | |
let displayName = headerRaw.replace(/^[#*\s-]+/g, '').trim(); | |
// ... (existing additional section mapping logic) ... | |
additionalSections[displayName] = { | |
content: sectionBlock, | |
type: determineSectionType(sectionBlock) | |
}; | |
} | |
}); | |
// If no sections were found, try to extract from the complete text | |
if (Object.keys(parsedData).length === 0) { | |
console.log('No sections found, attempting full-text extraction'); | |
// Try to extract grades and pass status from the raw text | |
const titleMatch = cleanedQaText.match(/TITLE[^:]*:\s*(\d+)\/100[^📋]*?(✅|❌|PASS|FAIL)/i); | |
const metaMatch = cleanedQaText.match(/META[^:]*:\s*(\d+)\/100[^📋]*?(✅|❌|PASS|FAIL)/i); | |
const h1Match = cleanedQaText.match(/H1[^:]*:\s*(\d+)\/100[^📋]*?(✅|❌|PASS|FAIL)/i); | |
const copyMatch = cleanedQaText.match(/COPY[^:]*:\s*(\d+)\/100[^📋]*?(✅|❌|PASS|FAIL)/i); | |
if (titleMatch) { | |
parsedData.title = { | |
grade: `${titleMatch[1]}/100`, | |
pass: titleMatch[2] === '✅' || titleMatch[2].toUpperCase() === 'PASS', | |
errors: titleMatch[2] === '✅' || titleMatch[2].toUpperCase() === 'PASS' ? ['No errors reported.'] : ['Violations detected'], | |
corrected: 'Content analysis extracted from full report', | |
rawContent: cleanedQaText | |
}; | |
console.log('Extracted TITLE data:', parsedData.title); | |
} | |
if (metaMatch) { | |
parsedData.meta = { | |
grade: `${metaMatch[1]}/100`, | |
pass: metaMatch[2] === '✅' || metaMatch[2].toUpperCase() === 'PASS', | |
errors: metaMatch[2] === '✅' || metaMatch[2].toUpperCase() === 'PASS' ? ['No errors reported.'] : ['Violations detected'], | |
corrected: 'Content analysis extracted from full report', | |
rawContent: cleanedQaText | |
}; | |
console.log('Extracted META data:', parsedData.meta); | |
} | |
if (h1Match) { | |
parsedData.h1 = { | |
grade: `${h1Match[1]}/100`, | |
pass: h1Match[2] === '✅' || h1Match[2].toUpperCase() === 'PASS', | |
errors: h1Match[2] === '✅' || h1Match[2].toUpperCase() === 'PASS' ? ['No errors reported.'] : ['Violations detected'], | |
corrected: 'Content analysis extracted from full report', | |
rawContent: cleanedQaText | |
}; | |
console.log('Extracted H1 data:', parsedData.h1); | |
} | |
if (copyMatch) { | |
parsedData.copy = { | |
grade: `${copyMatch[1]}/100`, | |
pass: copyMatch[2] === '✅' || copyMatch[2].toUpperCase() === 'PASS', | |
errors: copyMatch[2] === '✅' || copyMatch[2].toUpperCase() === 'PASS' ? ['No errors reported.'] : ['Violations detected'], | |
corrected: 'Content analysis extracted from full report', | |
rawContent: cleanedQaText | |
}; | |
console.log('Extracted COPY data:', parsedData.copy); | |
} | |
// Extract overall assessment | |
const overallMatch = cleanedQaText.match(/(?:OVERALL|Final).*?(\d+)\/100/i); | |
if (overallMatch) { | |
const overallGrade = parseInt(overallMatch[1]); | |
parsedData.overall = { | |
grade: `${overallGrade}/100`, | |
pass: overallGrade >= 80, | |
primaryIssue: overallGrade >= 80 ? 'All sections meet requirements' : 'Some violations detected', | |
rawContent: cleanedQaText | |
}; | |
console.log('Extracted OVERALL data:', parsedData.overall); | |
} | |
} | |
const finalReport: DetailedQaReport = { | |
title: parsedData.title || { ...defaultSection, errors: ['Title section not found in QA response'] }, | |
meta: parsedData.meta || { ...defaultSection, errors: ['Meta section not found in QA response'] }, | |
h1: parsedData.h1 || { ...defaultSection, errors: ['H1 section not found in QA response'] }, | |
copy: parsedData.copy || { ...defaultSection, errors: ['Copy section not found in QA response'] }, | |
overall: parsedData.overall || { grade: 'N/A', pass: false, primaryIssue: 'Overall assessment not found in QA response' }, | |
additionalSections: Object.keys(additionalSections).length > 0 ? additionalSections : undefined, | |
completeRawReport: qaText | |
}; | |
// Calculate overall pass/grade from individual sections if overall not found | |
if (!parsedData.overall && (parsedData.title || parsedData.meta || parsedData.h1 || parsedData.copy)) { | |
const validSections = [parsedData.title, parsedData.meta, parsedData.h1, parsedData.copy].filter(Boolean); | |
const allPass = validSections.every(section => section?.pass); | |
const grades = validSections.map(section => { | |
if (section?.grade && section.grade !== 'N/A') { | |
const match = section.grade.match(/(\d+)/); | |
return match ? parseInt(match[1]) : 0; | |
} | |
return 0; | |
}).filter(g => g > 0); | |
const avgGrade = grades.length > 0 ? Math.round(grades.reduce((a, b) => a + b, 0) / grades.length) : 0; | |
finalReport.overall = { | |
grade: avgGrade > 0 ? `${avgGrade}/100` : 'N/A', | |
pass: allPass && avgGrade >= 80, | |
primaryIssue: allPass ? 'All sections passed' : 'Some sections have violations', | |
rawContent: cleanedQaText | |
}; | |
console.log('Calculated overall from sections:', finalReport.overall); | |
} | |
console.log('Final QA parsing result:', { | |
title: finalReport.title?.grade, | |
meta: finalReport.meta?.grade, | |
h1: finalReport.h1?.grade, | |
copy: finalReport.copy?.grade, | |
overall: finalReport.overall?.grade, | |
overallPass: finalReport.overall?.pass | |
}); | |
return { | |
detailedQaReport: finalReport, | |
overallPass: finalReport.overall.pass, | |
overallGrade: finalReport.overall.grade | |
}; | |
}; | |
/** | |
* Parses a single section of the QA report (e.g., TITLE, H1) based on the actual QA Guard format. | |
* This is a more robust parser that handles variations in header format and content. | |
*/ | |
const parseActualQAGuardSection = (sectionBlock: string, sectionType: string): QaSectionResult | null => { | |
console.log(`Parsing ${sectionType} section with actual QA Guard format`); | |
console.log('Section block preview:', sectionBlock.substring(0, 200)); | |
// Extract grade and pass status from the actual QA Guard format | |
let grade = 'N/A'; | |
let pass = false; | |
let errors: string[] = ['No errors reported.']; | |
let corrected = 'Content analysis not available.'; | |
let detailedAssessment = ''; | |
let keyStrengths: string[] = []; | |
let recommendations: string[] = []; | |
let explanations = ''; | |
// COMPREHENSIVE HEADER PATTERN MATCHING | |
// Look for the actual QA Guard format: "### **TITLE** ✅ PASS" or "### **TITLE** ❌ FAIL" | |
// Also handle variations like "## **TITLE: PASS** ✅" or "## **TITLE: FAIL** ❌" | |
const headerPatterns = [ | |
/###\s*\*\*([^*]+)\*\*\s*(✅|❌)\s*(PASS|FAIL)/i, // ### **TITLE** ✅ PASS | |
/##\s*\*\*([^*]+):\s*(PASS|FAIL)\*\*\s*(✅|❌)/i, // ## **TITLE: PASS** ✅ | |
/##\s*\*\*([^*]+)\*\*\s*(PASS|FAIL)\s*(✅|❌)/i, // ## **TITLE** PASS ✅ | |
/###\s*\*\*([^*]+):\s*(PASS|FAIL)\*\*\s*(✅|❌)/i, // ### **TITLE: PASS** ✅ | |
/##\s*\*\*([^*]+)\*\*\s*(✅|❌)\s*(PASS|FAIL)/i, // ## **TITLE** ✅ PASS | |
/###\s*\*\*([^*]+)\*\*\s*(PASS|FAIL)/i, // ### **TITLE** PASS | |
/##\s*\*\*([^*]+)\*\*\s*(PASS|FAIL)/i, // ## **TITLE** PASS | |
/###\s*\*\*([^*]+):\s*(PASS|FAIL)/i, // ### **TITLE: PASS** | |
/##\s*\*\*([^*]+):\s*(PASS|FAIL)/i, // ## **TITLE: PASS** | |
/###\s*([^*]+)\s*(✅|❌)\s*(PASS|FAIL)/i, // ### TITLE ✅ PASS | |
/##\s*([^*]+)\s*(✅|❌)\s*(PASS|FAIL)/i, // ## TITLE ✅ PASS | |
/###\s*([^*]+):\s*(PASS|FAIL)\s*(✅|❌)/i, // ### TITLE: PASS ✅ | |
/##\s*([^*]+):\s*(PASS|FAIL)\s*(✅|❌)/i, // ## TITLE: PASS ✅ | |
/###\s*([^*]+)\s*(PASS|FAIL)/i, // ### TITLE PASS | |
/##\s*([^*]+)\s*(PASS|FAIL)/i, // ## TITLE PASS | |
/###\s*([^*]+):\s*(PASS|FAIL)/i, // ### TITLE: PASS | |
/##\s*([^*]+):\s*(PASS|FAIL)/i, // ## TITLE: PASS | |
]; | |
let headerMatch = null; | |
for (const pattern of headerPatterns) { | |
headerMatch = sectionBlock.match(pattern); | |
if (headerMatch) { | |
console.log('Found QA Guard header format:', headerMatch[0]); | |
// Determine pass status from various indicators in the match | |
const passIndicators = [headerMatch[2], headerMatch[3]].filter(Boolean); | |
pass = passIndicators.some(indicator => | |
indicator === '✅' || indicator.toUpperCase() === 'PASS' | |
); | |
break; | |
} | |
} | |
// COMPREHENSIVE GRADE PATTERN MATCHING | |
// Look for grade in various formats: "- **Grade:** 100/100", "Grade: 100/100", etc. | |
const gradePatterns = [ | |
/-?\s*\*\*Grade:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Grade:** 100/100 | |
/-?\s*\*\*Grade\*\*:\s*(\d+(?:\.\d+)?)\/100/i, // - **Grade**: 100/100 | |
/-?\s*Grade:\s*(\d+(?:\.\d+)?)\/100/i, // - Grade: 100/100 | |
/-?\s*Grade\s*:\s*(\d+(?:\.\d+)?)\/100/i, // - Grade : 100/100 | |
/\*\*Grade:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // **Grade:** 100/100 | |
/\*\*Grade\*\*:\s*(\d+(?:\.\d+)?)\/100/i, // **Grade**: 100/100 | |
/Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Grade: 100/100 | |
/Grade\s*:\s*(\d+(?:\.\d+)?)\/100/i, // Grade : 100/100 | |
/-?\s*\*\*Score:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Score:** 100/100 | |
/-?\s*\*\*Rating:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Rating:** 100/100 | |
/-?\s*\*\*Mark:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Mark:** 100/100 | |
/-?\s*\*\*Points:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Points:** 100/100 | |
/-?\s*\*\*(\d+(?:\.\d+)?)\/100\s*points?\*\*/i, // - **100/100 points** | |
/-?\s*(\d+(?:\.\d+)?)\/100\s*points?/i, // - 100/100 points | |
/-?\s*\*\*(\d+(?:\.\d+)?)\/100\*\*/i, // - **100/100** | |
/-?\s*(\d+(?:\.\d+)?)\/100/i, // - 100/100 | |
/\*\*(\d+(?:\.\d+)?)\/100\*\*/i, // **100/100** | |
/(\d+(?:\.\d+)?)\/100/i, // 100/100 | |
]; | |
let gradeMatch = null; | |
for (const pattern of gradePatterns) { | |
gradeMatch = sectionBlock.match(pattern); | |
if (gradeMatch) { | |
grade = `${gradeMatch[1]}/100`; | |
console.log('Found grade:', grade); | |
break; | |
} | |
} | |
// COMPREHENSIVE COMPLIANCE PATTERN MATCHING | |
// Look for compliance status in various formats | |
const compliancePatterns = [ | |
/-?\s*\*\*Compliance:\*\*\s*([^\n]+)/i, // - **Compliance:** Full compliance | |
/-?\s*\*\*Status:\*\*\s*([^\n]+)/i, // - **Status:** Passed | |
/-?\s*\*\*Result:\*\*\s*([^\n]+)/i, // - **Result:** Successful | |
/-?\s*\*\*Assessment:\*\*\s*([^\n]+)/i, // - **Assessment:** Compliant | |
/-?\s*\*\*Evaluation:\*\*\s*([^\n]+)/i, // - **Evaluation:** Pass | |
/-?\s*\*\*Check:\*\*\s*([^\n]+)/i, // - **Check:** OK | |
/-?\s*\*\*Verification:\*\*\s*([^\n]+)/i, // - **Verification:** Valid | |
/-?\s*\*\*Review:\*\*\s*([^\n]+)/i, // - **Review:** Approved | |
/-?\s*\*\*Analysis:\*\*\s*([^\n]+)/i, // - **Analysis:** Compliant | |
/-?\s*\*\*Summary:\*\*\s*([^\n]+)/i, // - **Summary:** Pass | |
]; | |
let complianceMatch = null; | |
for (const pattern of compliancePatterns) { | |
complianceMatch = sectionBlock.match(pattern); | |
if (complianceMatch) { | |
const compliance = complianceMatch[1].trim(); | |
console.log('Found compliance:', compliance); | |
// Determine pass status from compliance text | |
const failIndicators = ['violation', 'fail', 'error', 'non-compliant', 'rejected', 'invalid', 'incorrect', 'missing', 'below', 'above', 'out of range']; | |
const passIndicators = ['compliance', 'pass', 'success', 'valid', 'correct', 'approved', 'compliant', 'within range', 'meets', 'satisfies']; | |
const lowerCompliance = compliance.toLowerCase(); | |
if (failIndicators.some(indicator => lowerCompliance.includes(indicator))) { | |
pass = false; | |
} else if (passIndicators.some(indicator => lowerCompliance.includes(indicator))) { | |
pass = true; | |
} | |
break; | |
} | |
} | |
// COMPREHENSIVE ANALYSIS PATTERN MATCHING | |
// Look for analysis content in various formats | |
const analysisPatterns = [ | |
/-?\s*\*\*Analysis:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Analysis:** Detailed text | |
/-?\s*\*\*Review:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Review:** Detailed text | |
/-?\s*\*\*Assessment:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Assessment:** Detailed text | |
/-?\s*\*\*Evaluation:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Evaluation:** Detailed text | |
/-?\s*\*\*Summary:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Summary:** Detailed text | |
/-?\s*\*\*Details:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Details:** Detailed text | |
/-?\s*\*\*Breakdown:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Breakdown:** Detailed text | |
/-?\s*\*\*Explanation:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Explanation:** Detailed text | |
/-?\s*\*\*Comments:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Comments:** Detailed text | |
/-?\s*\*\*Notes:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Notes:** Detailed text | |
]; | |
let analysisMatch = null; | |
for (const pattern of analysisPatterns) { | |
analysisMatch = sectionBlock.match(pattern); | |
if (analysisMatch) { | |
detailedAssessment = analysisMatch[1].trim(); | |
console.log('Found analysis:', detailedAssessment.substring(0, 100)); | |
break; | |
} | |
} | |
// COMPREHENSIVE ERROR PATTERN MATCHING | |
// Look for error information in various formats | |
const errorPatterns = [ | |
/-?\s*\*\*Error:\*\*\s*([^\n]+)/i, // - **Error:** Error message | |
/-?\s*\*\*Errors:\*\*\s*([^\n]+)/i, // - **Errors:** Error messages | |
/-?\s*\*\*Issue:\*\*\s*([^\n]+)/i, // - **Issue:** Issue description | |
/-?\s*\*\*Issues:\*\*\s*([^\n]+)/i, // - **Issues:** Issue descriptions | |
/-?\s*\*\*Problem:\*\*\s*([^\n]+)/i, // - **Problem:** Problem description | |
/-?\s*\*\*Problems:\*\*\s*([^\n]+)/i, // - **Problems:** Problem descriptions | |
/-?\s*\*\*Violation:\*\*\s*([^\n]+)/i, // - **Violation:** Violation details | |
/-?\s*\*\*Violations:\*\*\s*([^\n]+)/i, // - **Violations:** Violation details | |
/-?\s*\*\*Warning:\*\*\s*([^\n]+)/i, // - **Warning:** Warning message | |
/-?\s*\*\*Warnings:\*\*\s*([^\n]+)/i, // - **Warnings:** Warning messages | |
/-?\s*\*\*Concern:\*\*\s*([^\n]+)/i, // - **Concern:** Concern details | |
/-?\s*\*\*Concerns:\*\*\s*([^\n]+)/i, // - **Concerns:** Concern details | |
]; | |
let errorMatch = null; | |
for (const pattern of errorPatterns) { | |
errorMatch = sectionBlock.match(pattern); | |
if (errorMatch) { | |
errors = [errorMatch[1].trim()]; | |
console.log('Found error:', errors[0]); | |
break; | |
} | |
} | |
// ENHANCED: Look for multi-line violation lists | |
const violationsMatch = sectionBlock.match(/-?\s*\*\*(?:Structure|Major|Minor)\s+violations?\*\*:\s*([\s\S]*?)(?=\n-?\s*\*\*|\n\n|---|$)/i); | |
if (violationsMatch) { | |
const violationText = violationsMatch[1].trim(); | |
// Split by lines that start with the violation marker | |
const violationErrors = violationText.split(/\n\s*(?:-|\d+\.)\s*(?:❌|\⚠️)?\s*\*\*/g) | |
.map(line => { | |
// Clean up the line to get the core violation text | |
return line.replace(/MAJOR VIOLATION \([^)]+\):/, '') | |
.replace(/MINOR VIOLATION \([^)]+\):/, '') | |
.replace(/\*\*$/, '') | |
.trim(); | |
}) | |
.filter(line => line.length > 10); // Filter out empty or trivial lines | |
if (violationErrors.length > 0) { | |
if (errors[0] === 'No errors reported.') { | |
errors = violationErrors; | |
} else { | |
// Prepend violation details to any existing errors | |
errors = [...violationErrors, ...errors]; | |
} | |
console.log('Found detailed violation errors:', violationErrors); | |
} | |
} | |
// COMPREHENSIVE CORRECTED CONTENT PATTERN MATCHING | |
// Look for corrected content in various formats | |
const correctedPatterns = [ | |
/\*\*CORRECTED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, // **CORRECTED META:**\n```\ncontent\n``` | |
/\*\*CORRECTED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, // **CORRECTED META:**\ncontent | |
/\*\*FIXED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, // **FIXED META:**\n```\ncontent\n``` | |
/\*\*FIXED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, // **FIXED META:**\ncontent | |
/\*\*REVISED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, // **REVISED META:**\n```\ncontent\n``` | |
/\*\*REVISED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, // **REVISED META:**\ncontent | |
/\*\*UPDATED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, // **UPDATED META:**\n```\ncontent\n``` | |
/\*\*UPDATED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, // **UPDATED META:**\ncontent | |
/\*\*SUGGESTED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, // **SUGGESTED META:**\n```\ncontent\n``` | |
/\*\*SUGGESTED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, // **SUGGESTED META:**\ncontent | |
/\*\*RECOMMENDED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, // **RECOMMENDED META:**\n```\ncontent\n``` | |
/\*\*RECOMMENDED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, // **RECOMMENDED META:**\ncontent | |
/-?\s*\*\*Corrected:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Corrected:** content | |
/-?\s*\*\*Fixed:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Fixed:** content | |
/-?\s*\*\*Revised:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Revised:** content | |
/-?\s*\*\*Updated:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Updated:** content | |
/-?\s*\*\*Suggested:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Suggested:** content | |
/-?\s*\*\*Recommended:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Recommended:** content | |
]; | |
let correctedMatch = null; | |
for (const pattern of correctedPatterns) { | |
correctedMatch = sectionBlock.match(pattern); | |
if (correctedMatch) { | |
corrected = correctedMatch[1].trim(); | |
console.log('Found corrected content:', corrected.substring(0, 100)); | |
break; | |
} | |
} | |
// ENHANCED KEY STRENGTHS AND RECOMMENDATIONS EXTRACTION | |
// Extract key strengths and recommendations from the analysis and compliance text | |
if (detailedAssessment) { | |
// Look for positive indicators in analysis | |
const positiveIndicators = [ | |
'compliance', 'compliant', 'proper', '✓', 'check', 'valid', 'correct', 'appropriate', | |
'meets', 'satisfies', 'within range', 'successful', 'approved', 'pass', 'good', 'excellent', | |
'optimal', 'ideal', 'perfect', 'complete', 'thorough', 'comprehensive', 'effective' | |
]; | |
const negativeIndicators = [ | |
'violation', 'error', 'fail', 'problem', 'issue', 'concern', 'warning', 'missing', | |
'below', 'above', 'out of range', 'incorrect', 'invalid', 'non-compliant', 'rejected', | |
'insufficient', 'inadequate', 'poor', 'weak', 'deficient' | |
]; | |
const lowerAnalysis = detailedAssessment.toLowerCase(); | |
// Extract key strengths | |
if (positiveIndicators.some(indicator => lowerAnalysis.includes(indicator))) { | |
keyStrengths.push('Meets compliance requirements'); | |
} | |
if (lowerAnalysis.includes('keyword') && (lowerAnalysis.includes('included') || lowerAnalysis.includes('present'))) { | |
keyStrengths.push('Proper keyword integration'); | |
} | |
if (lowerAnalysis.includes('tone') && lowerAnalysis.includes('appropriate')) { | |
keyStrengths.push('Appropriate tone maintained'); | |
} | |
if (lowerAnalysis.includes('character') && lowerAnalysis.includes('within')) { | |
keyStrengths.push('Character count within requirements'); | |
} | |
if (lowerAnalysis.includes('length') && lowerAnalysis.includes('✓')) { | |
keyStrengths.push('Length requirements met'); | |
} | |
if (lowerAnalysis.includes('structure') && lowerAnalysis.includes('proper')) { | |
keyStrengths.push('Proper structure maintained'); | |
} | |
// Extract recommendations | |
if (negativeIndicators.some(indicator => lowerAnalysis.includes(indicator))) { | |
recommendations.push('Address identified violations'); | |
} | |
if (lowerAnalysis.includes('character') && (lowerAnalysis.includes('count') || lowerAnalysis.includes('length'))) { | |
recommendations.push('Adjust character count to meet requirements'); | |
} | |
if (lowerAnalysis.includes('word') && lowerAnalysis.includes('count')) { | |
recommendations.push('Adjust word count to meet requirements'); | |
} | |
if (lowerAnalysis.includes('keyword') && lowerAnalysis.includes('missing')) { | |
recommendations.push('Include required keywords'); | |
} | |
if (lowerAnalysis.includes('tone') && lowerAnalysis.includes('inappropriate')) { | |
recommendations.push('Adjust tone to meet requirements'); | |
} | |
if (lowerAnalysis.includes('structure') && lowerAnalysis.includes('improve')) { | |
recommendations.push('Improve content structure'); | |
} | |
} | |
// FALLBACK PATTERN MATCHING | |
// If no grade found, try alternative patterns | |
if (grade === 'N/A') { | |
const fallbackGradePatterns = [ | |
/Grade:\s*(\d+(?:\.\d+)?)\/100/i, | |
/Score:\s*(\d+(?:\.\d+)?)\/100/i, | |
/Rating:\s*(\d+(?:\.\d+)?)\/100/i, | |
/Mark:\s*(\d+(?:\.\d+)?)\/100/i, | |
/Points:\s*(\d+(?:\.\d+)?)\/100/i, | |
/(\d+(?:\.\d+)?)\/100/i, | |
]; | |
for (const pattern of fallbackGradePatterns) { | |
const match = sectionBlock.match(pattern); | |
if (match) { | |
grade = `${match[1]}/100`; | |
console.log('Found fallback grade:', grade); | |
break; | |
} | |
} | |
} | |
// INFER PASS STATUS FROM GRADE IF NOT DETERMINED | |
if (grade !== 'N/A' && !headerMatch && !complianceMatch) { | |
const gradeNum = parseFloat(grade.split('/')[0]); | |
pass = gradeNum >= 80; | |
console.log('Inferred pass status from grade:', pass); | |
} | |
// FINAL PASS STATUS DETERMINATION | |
// If still no pass status, check for pass/fail indicators in text | |
if (!headerMatch && !complianceMatch && grade === 'N/A') { | |
const lowerSection = sectionBlock.toLowerCase(); | |
if (lowerSection.includes('pass') && !lowerSection.includes('fail')) { | |
pass = true; | |
} else if (lowerSection.includes('fail')) { | |
pass = false; | |
} else if (lowerSection.includes('✅') && !lowerSection.includes('❌')) { | |
pass = true; | |
} else if (lowerSection.includes('❌')) { | |
pass = false; | |
} | |
} | |
// UPDATE ERRORS BASED ON PASS STATUS | |
if (pass && errors[0] === 'No errors reported.') { | |
// Keep as is | |
} else if (!pass && errors[0] === 'No errors reported.') { | |
errors = ['Violations detected']; | |
} | |
console.log(`Final ${sectionType} result - Grade: ${grade}, Pass: ${pass}, Errors: ${errors.length}`); | |
return { | |
grade, | |
pass, | |
errors, | |
corrected, | |
detailedAssessment: detailedAssessment || undefined, | |
keyStrengths: keyStrengths.length > 0 ? keyStrengths : undefined, | |
recommendations: recommendations.length > 0 ? recommendations : undefined, | |
explanations: explanations || undefined, | |
rawContent: sectionBlock | |
}; | |
}; | |
/** | |
* Runs the Dify workflow for a given input row, with retries for transient errors. | |
* @param inputs - The data from a CSV row. | |
* @returns A promise that resolves to the processed and cleaned results. | |
*/ | |
export const runWorkflow = async (inputs: ApiInput): Promise<ProcessedResult> => { | |
let lastError: Error = new Error('Workflow failed after all retries.'); | |
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { | |
let responseText = ''; | |
try { | |
const payload = { | |
inputs, | |
response_mode: 'streaming', | |
user: API_USER, | |
}; | |
const response = await fetch(API_URL, { | |
method: 'POST', | |
headers: { | |
'Authorization': `Bearer ${API_TOKEN}`, | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify(payload), | |
}); | |
if (!response.ok) { | |
responseText = await response.text(); | |
// Check for retryable HTTP status codes. | |
if (RETRYABLE_STATUS_CODES.includes(response.status)) { | |
throw new WorkflowError(`RETRYABLE_HTTP_${response.status}`, `Temporary service issue (HTTP ${response.status}).`, 'network', responseText); | |
} | |
// For other HTTP errors, fail immediately. | |
throw new WorkflowError(`HTTP_${response.status}`, `API request failed (HTTP ${response.status}).`, 'api', responseText); | |
} | |
if (!response.body) { | |
throw new WorkflowError('EMPTY_RESPONSE', 'Empty response from API.', 'network'); | |
} | |
const reader = response.body.getReader(); | |
const decoder = new TextDecoder(); | |
let streamContent = ''; | |
while (true) { | |
const { done, value } = await reader.read(); | |
if (done) break; | |
streamContent += decoder.decode(value, { stream: true }); | |
} | |
// The full stream content becomes our responseText for error logging | |
responseText = streamContent; | |
const lines = streamContent.trim().split('\n'); | |
const finishedLine = lines.find(line => line.includes('"event": "workflow_finished"')) || ''; | |
if (!finishedLine) { | |
// The gateway might have returned an HTML error page instead of a stream | |
if (streamContent.trim().toLowerCase().startsWith('<html')) { | |
throw new WorkflowError('RETRYABLE_HTML_RESPONSE', 'Service returned an HTML error page.', 'stream', streamContent.slice(0, 1000)); | |
} | |
console.error('Full stream content:', streamContent); | |
throw new WorkflowError('FINISH_EVENT_MISSING', 'Workflow did not finish successfully.', 'stream', streamContent.slice(0, 1000)); | |
} | |
const jsonString = finishedLine.replace(/^data: /, ''); | |
const finishedEventData = JSON.parse(jsonString); | |
if (finishedEventData.data.status !== 'succeeded') { | |
const apiError = finishedEventData.data.error || 'Unknown'; | |
const isOverloaded = typeof apiError === 'string' && (apiError.toLowerCase().includes('overloaded') || apiError.toLowerCase().includes('gateway time-out')); | |
// If it's a known transient error, mark it as retryable. | |
if (isOverloaded) { | |
throw new WorkflowError('RETRYABLE_API_ERROR', 'Service overloaded. Retrying...', 'api', String(apiError)); | |
} | |
// Otherwise, it's a permanent failure for this row. | |
throw new WorkflowError('WORKFLOW_FAILED', `Workflow failed: ${apiError}`, 'api', String(apiError)); | |
} | |
const outputs: ApiResponseOutput = finishedEventData.data.outputs; | |
if (!outputs || Object.keys(outputs).length === 0) { | |
throw new WorkflowError('EMPTY_OUTPUTS', 'Workflow succeeded but returned empty outputs.', 'parse'); | |
} | |
const rawQaReport = outputs.qa_gaurd || 'QA report not available.'; | |
console.log('QA Report length:', rawQaReport.length); | |
const { detailedQaReport, overallPass, overallGrade } = parseNewQaReport(rawQaReport); | |
console.log('Final Parsed QA - Pass:', overallPass, 'Grade:', overallGrade); | |
// Success, return the result and exit the loop. | |
return { | |
generatedTitle: cleanResponseText(outputs.title), | |
generatedH1: cleanResponseText(outputs.h1), | |
generatedCopy: cleanResponseText(outputs.copy), | |
generatedMeta: cleanResponseText(outputs.meta), | |
qaReport: rawQaReport, | |
detailedQaReport, | |
overallPass, | |
overallGrade, | |
}; | |
} catch (error) { | |
lastError = error instanceof Error ? error : new Error(String(error)); | |
const isRetryable = lastError instanceof WorkflowError && lastError.code.startsWith('RETRYABLE_'); | |
if (isRetryable && attempt < MAX_RETRIES) { | |
// Exponential backoff with jitter: 1s, 2s, 4s, ... + random | |
const delayMs = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1); | |
console.warn(`Attempt ${attempt}/${MAX_RETRIES} failed due to a transient error. Retrying in ~${Math.round(delayMs / 1000)}s...`, { error: lastError.message }); | |
await delay(delayMs); | |
continue; // Move to the next attempt | |
} | |
// For non-retryable errors, or if we've exhausted retries, break the loop to throw the error. | |
console.error(`Failed to process row. Attempt ${attempt}/${MAX_RETRIES}. Error: ${lastError.message}`); | |
if (responseText) { | |
// Log the problematic response that caused the failure | |
console.error('Problematic Response:', responseText); | |
} | |
break; | |
} | |
} | |
// If the loop finished without returning, it means all attempts failed. | |
// We re-throw the last captured error, making it more user-friendly if it was a transient one. | |
if (lastError instanceof WorkflowError && lastError.code.startsWith('RETRYABLE_')) { | |
throw new WorkflowError('SERVICE_UNAVAILABLE', `API service is temporarily unavailable. Tried ${MAX_RETRIES} times. Please try again later.`, 'api', lastError.debug || lastError.stack); | |
} | |
throw lastError; | |
}; | |