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 let pass = false; let passMatch = null; // Try various pass patterns in order of specificity const passPatterns = [ /-\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('true') || passValue.includes('✅') || passValue === 'yes' || passValue === 'passed' || passValue === 'pass'; 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.']; } } // 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)); 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 } => { // ROBUST OVERALL GRADE EXTRACTION - handles multiple formats let grade = 'N/A'; let gradeMatch = null; const overallGradePatterns = [ /-\s*\*\*Final Grade:\*\*\s*(.*)/, // - **Final Grade:** 100/100 /•\s*\*\*Final Grade:\*\*\s*(.*)/, // • **Final Grade:** 100/100 /\*\s*\*\*Final Grade:\*\*\s*(.*)/, // * **Final Grade:** 100/100 /-\s*\*\*Total Grade:\*\*\s*(.*)/, // - **Total Grade:** 100/100 /•\s*\*\*Total Grade:\*\*\s*(.*)/, // • **Total Grade:** 100/100 /\*\s*\*\*Total Grade:\*\*\s*(.*)/, // * **Total Grade:** 100/100 /(?:•|-|\*)\s*\*\*(?:Final|Total|Overall)?\s*Grade\*\*:?:\s*(.*)/i, // Generic grade /(?:•|-|\*)\s*(?:Final|Total|Overall)?\s*Grade:?:\s*(.*)/i, // No bold /(?:Final|Total|Overall)\s*Grade:?:\s*(\d+\/\d+|\d+)/im, // Anywhere in text /(\d+\/\d+)\s*(?:final|total|overall)?/im // Number first ]; for (const pattern of overallGradePatterns) { gradeMatch = sectionBlock.match(pattern); if (gradeMatch) { grade = gradeMatch[1].trim(); break; } } console.log('Overall grade match:', gradeMatch, 'Final grade:', grade); // ROBUST OVERALL PASS EXTRACTION - handles multiple formats let pass = false; let passMatch = null; const overallPassPatterns = [ // Exact format variations found in logs /-\s*\*\*Overall Pass:\*\*\s*(.*)/i, // - **Overall Pass:** true /•\s*\*\*Overall Pass:\*\*\s*(.*)/i, // • **Overall Pass:** true /\*\s*\*\*Overall Pass:\*\*\s*(.*)/i, // * **Overall Pass:** true /•\s*\*\*All Sections Pass:\*\*\s*(.*)/i, // • **All Sections Pass:** true /-\s*\*\*All Sections Pass:\*\*\s*(.*)/i, // - **All Sections Pass:** true /\*\s*\*\*All Sections Pass:\*\*\s*(.*)/i, // * **All Sections Pass:** true /•\s*\*\*Final Pass:\*\*\s*(.*)/i, // • **Final Pass:** true /-\s*\*\*Final Pass:\*\*\s*(.*)/i, // - **Final Pass:** true /\*\s*\*\*Final Pass:\*\*\s*(.*)/i, // * **Final Pass:** true // Generic patterns with flexible formatting /(?:•|-|\*)\s*\*\*(?:Overall\s+|All\s+Sections\s+|Final\s+)?Pass\*\*:?:\s*(.*)/i, /(?:•|-|\*)\s*(?:Overall\s+|All\s+Sections\s+|Final\s+)?Pass:?:\s*(.*)/i, // Anywhere in text patterns /Pass:?:\s*(true|false|✅|❌|TRUE|FALSE)/im, /Overall\s*Pass:?:\s*(true|false|✅|❌|TRUE|FALSE)/im, /All\s*Sections\s*Pass:?:\s*(true|false|✅|❌|TRUE|FALSE)/im, /(true|false|✅|❌|TRUE|FALSE)\s*(?:overall|pass)/im, // Handle capitalized boolean values /Pass:?:\s*(True|False|TRUE|FALSE)/im, /Overall\s*Pass:?:\s*(True|False|TRUE|FALSE)/im ]; for (const pattern of overallPassPatterns) { passMatch = sectionBlock.match(pattern); if (passMatch) { const passValue = passMatch[1].toLowerCase().trim(); pass = passValue.includes('true') || passValue.includes('✅') || passValue === 'yes' || passValue === 'passed' || passValue === 'pass'; break; } } console.log('Overall pass match:', passMatch, 'Final pass:', pass); // Look for various primary issue formats const explanationMatch = sectionBlock.match(/\*\*Overall Pass\*\*:\s*[^()]*\(([^)]+)\)/); const statusMatch = sectionBlock.match(/(?:•|-|\*)\s*\*\*(?:Pipeline\s+)?Status\*\*?:?\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/); const primaryIssueMatch = sectionBlock.match(/(?:•|-|\*)\s*\*\*(?:Primary\s+)?Issue\*\*?:?\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/) || sectionBlock.match(/(?:•|-|\*)\s*(?:Primary\s+)?Issue:?:\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/); const errorsMatch = sectionBlock.match(/(?:•|-|\*)\s*\*\*Total\s+Errors?\*\*?:?\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/); const totalSectionsMatch = sectionBlock.match(/Total\s*Sections\s*Passing:?:\s*([^\n]+)/i); let primaryIssue = 'All sections passed successfully.'; if (explanationMatch) { primaryIssue = explanationMatch[1].trim(); } else if (statusMatch) { primaryIssue = statusMatch[1].trim(); } else if (primaryIssueMatch) { primaryIssue = primaryIssueMatch[1].trim(); } else if (errorsMatch) { const errorText = errorsMatch[1].trim(); if (errorText !== '[]' && errorText !== '') { primaryIssue = `Errors found: ${errorText}`; } } if (totalSectionsMatch) { primaryIssue = `${primaryIssue} | Total Sections Passing: ${totalSectionsMatch[1].trim()}`; } console.log('Primary issue extraction - explanation:', explanationMatch, 'status:', statusMatch, 'issue:', primaryIssueMatch, 'Final issue:', primaryIssue); // 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; console.log('Setting overall data - grade:', grade, 'pass:', pass, 'primaryIssue:', primaryIssue); return { grade, pass, primaryIssue, detailedAssessment: detailedAssessmentMatch ? detailedAssessmentMatch[1].trim() : undefined, explanations: explanationsMatch ? explanationsMatch[1].trim() : undefined, keyStrengths: strengthsList, recommendations: recommendationsList, 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' }; } // Check if it's the new markdown format (starts with ##) or contains **TITLE** style sections if (cleanedQaText.startsWith('##') || cleanedQaText.includes('**TITLE**') || cleanedQaText.includes('### **') || cleanedQaText.includes('### TITLE')) { console.log('Using enhanced markdown parser for QA report'); console.log('QA text starts with:', cleanedQaText.substring(0, 200)); // Handle different section header formats let sections; if (cleanedQaText.includes('### **')) { // Split on ### ** and keep the ** in the section content sections = cleanedQaText.split(/(?=### \*\*)/g).slice(1); console.log('Splitting on ### ** (keeping section headers)'); } else if (cleanedQaText.includes('**TITLE**')) { // Split on bold section headers like **TITLE**, **META**, etc. sections = cleanedQaText.split(/(?=\*\*(?:TITLE|META|H1|COPY|OVERALL|ASSESSMENT|CORRECTED COPY)[^*]*\*\*)/g).slice(1); console.log('Splitting on **SECTION** headers'); } else if (cleanedQaText.includes('### ')) { // Split generic ### headers sections = cleanedQaText.split(/(?=###\s+)/g).slice(1); console.log('Splitting on generic ### headers'); } else { sections = cleanedQaText.split('## ').slice(1); console.log('Splitting on ## headers'); } console.log('Found sections:', sections.length); sections.forEach((section, index) => { console.log(`Section ${index}:`, section.substring(0, 100)); }); const parsedData: Partial = {}; let correctedCopyFromSeparateSection = ''; const additionalSections: { [sectionName: string]: { content: string; type: 'assessment' | 'strengths' | 'recommendations' | 'explanations' | 'other'; } } = {}; // Special handling for single-section format like "## GRADE REPORT" if (sections.length === 1 && (sections[0].includes('GRADE REPORT') || sections[0].includes('QUALITY ASSURANCE'))) { console.log('Detected single-section format, parsing embedded sections'); return parseSingleSectionFormat(sections[0], defaultReport); } sections.forEach(sectionBlock => { const lines = sectionBlock.trim().split('\n'); let headerRaw = lines[0].trim(); let header = headerRaw.toLowerCase(); // Clean up header - remove markdown formatting and punctuation header = header.replace(/^#+\s*/, '').replace(/\*\*/g, '').replace(/[:\-–]+$/, '').trim(); console.log('Processing header:', header); // Enhanced section parsing to capture ALL content if (header.includes('title')) { console.log('Parsing title section with enhanced content capture'); parsedData.title = parseEnhancedSection(sectionBlock); } else if (header.includes('meta')) { console.log('Parsing meta section with enhanced content capture'); parsedData.meta = parseEnhancedSection(sectionBlock); } else if (header.includes('h1')) { console.log('Parsing h1 section with enhanced content capture'); parsedData.h1 = parseEnhancedSection(sectionBlock); } else if (header.includes('copy') && !header.includes('corrected')) { console.log('Parsing copy section with enhanced content capture'); parsedData.copy = parseEnhancedSection(sectionBlock); } else if (header.includes('corrected') && header.includes('copy')) { console.log('Capturing separate CORRECTED COPY section'); correctedCopyFromSeparateSection = lines.slice(1).join('\n').trim(); } else if (header.includes('overall') || header.includes('assessment') || header.includes('pipeline')) { console.log('Parsing overall section with enhanced content capture'); console.log('Overall section text:', sectionBlock.substring(0, 300)); // Enhanced overall section parsing to capture ALL content const enhancedOverall = parseEnhancedOverallSection(sectionBlock); parsedData.overall = enhancedOverall; } else { // Capture any additional sections that don't match standard patterns console.log('Capturing additional section:', header); const sectionType = determineSectionType(sectionBlock); additionalSections[header] = { content: sectionBlock, type: sectionType }; } }); 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' }, additionalSections: Object.keys(additionalSections).length > 0 ? additionalSections : undefined, completeRawReport: qaText }; // If we saw a separate CORRECTED COPY section, populate copy.corrected with it when useful if (correctedCopyFromSeparateSection) { const cleanedCorrected = correctedCopyFromSeparateSection.replace(/^###.*$/m, '').trim(); if (!finalReport.copy.corrected || finalReport.copy.corrected.length < 20) { finalReport.copy.corrected = cleanedCorrected; } } // If no explicit overall section was found, check for inline overall results at end of text if (!parsedData.overall) { // Look for overall results anywhere in the full text (sometimes appears at the end) const inlineOverallGradeMatch = cleanedQaText.match(/##?\s*OVERALL\s*(?:PIPELINE\s*)?GRADE:?:\s*([^#\n]+)/i); const inlineOverallPassMatch = cleanedQaText.match(/##?\s*OVERALL\s*(?:PIPELINE\s*)?PASS:?:\s*([^#\n(]+)/i); if (inlineOverallGradeMatch || inlineOverallPassMatch) { let grade = 'N/A'; let pass = false; if (inlineOverallGradeMatch) { grade = inlineOverallGradeMatch[1].trim(); } if (inlineOverallPassMatch) { const passValue = inlineOverallPassMatch[1].toLowerCase().trim(); pass = passValue.includes('true') || passValue.includes('✅') || passValue === 'yes' || passValue === 'passed'; } parsedData.overall = { grade, pass, primaryIssue: pass ? 'All sections passed successfully.' : 'Overall assessment failed.', rawContent: cleanedQaText }; console.log('Found inline overall results - grade:', grade, 'pass:', pass); } else { // Calculate overall pass from individual sections const allSectionsPassed = finalReport.title.pass && finalReport.meta.pass && finalReport.h1.pass && finalReport.copy.pass; // Calculate average grade if all sections have numeric grades let averageGrade = 'N/A'; const grades = [finalReport.title.grade, finalReport.meta.grade, finalReport.h1.grade, finalReport.copy.grade]; const numericGrades = grades.filter(g => g !== 'N/A' && g !== undefined) .map(g => { const match = String(g).match(/(\d+(?:\.\d+)?)/); return match ? parseFloat(match[1]) : null; }) .filter(g => g !== null) as number[]; if (numericGrades.length === 4) { const avg = numericGrades.reduce((sum, grade) => sum + grade, 0) / numericGrades.length; averageGrade = `${avg.toFixed(2)}/100`; } parsedData.overall = { grade: averageGrade, pass: allSectionsPassed, primaryIssue: allSectionsPassed ? 'All sections passed successfully.' : 'One or more sections failed.', rawContent: cleanedQaText }; console.log('Calculated overall from individual sections - pass:', allSectionsPassed, 'grade:', averageGrade); } // Update the final report with the calculated/found overall data finalReport.overall = parsedData.overall; } console.log('Final parsed QA data:', finalReport.overall); console.log('Setting overallPass:', finalReport.overall.pass, 'overallGrade:', finalReport.overall.grade); console.log('Additional sections captured:', Object.keys(additionalSections)); return { detailedQaReport: finalReport, overallPass: finalReport.overall.pass, overallGrade: finalReport.overall.grade }; } else { // Parse the new structured format return parseStructuredQaReport(cleanedQaText); } }; /** * 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('