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 ... blocks from a string. */ const cleanResponseText = (text: string): string => { if (typeof text !== 'string') return ''; return text.replace(/[\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 = {}; 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 = {}; 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 => { 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('