ACE / services /difyService.ts
Severian's picture
Update services/difyService.ts
7b642ea verified
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;
};