| 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]; |
|
|
| |
| const delay = (ms: number) => new Promise(res => setTimeout(res, ms + Math.random() * 500)); |
|
|
| |
| 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; |
| } |
| } |
|
|
| |
| |
| |
| const cleanResponseText = (text: string): string => { |
| if (typeof text !== 'string') return ''; |
| return text.replace(/<think>[\s\S]*?<\/think>/g, '').trim(); |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| const parseSection = (sectionText: string): QaSectionResult => { |
| console.log('Parsing section text:', sectionText.substring(0, 200)); |
| |
| |
| let grade = 'N/A'; |
| let gradeMatch = null; |
| |
| |
| const gradePatterns = [ |
| /-\s*\*\*Grade:\*\*\s*(.*)/, |
| /•\s*\*\*Grade:\*\*\s*(.*)/, |
| /\*\s*\*\*Grade:\*\*\s*(.*)/, |
| /-\s*\*\*Grade\*\*:\s*(.*)/, |
| /•\s*\*\*Grade\*\*:\s*(.*)/, |
| /\*\s*\*\*Grade\*\*:\s*(.*)/, |
| /(?:•|-|\*)\s*\*\*Grade\*\*:?:\s*(.*)/, |
| /(?:•|-|\*)\s*Grade:?:\s*(.*)/, |
| /Grade:?:\s*(\d+\/\d+|\d+)/m, |
| /(\d+\/\d+)\s*(?:grade|Grade)/ |
| ]; |
| |
| 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); |
| |
| |
| let pass = false; |
| let passMatch = null; |
| |
| |
| const passPatterns = [ |
| |
| /###\s*\*\*[^:]+:\s*(PASS|FAIL)\s*\*\*\s*(✅|❌)/i, |
| /###\s*\*\*[^:]+:\s*(PASS|FAIL)\s*\*\*/i, |
| /\*\*[^:]+:\s*(PASS|FAIL)\s*\*\*\s*(✅|❌)/i, |
| /\*\*[^:]+:\s*(PASS|FAIL)\s*\*\*/i, |
| |
| |
| /-\s*\*\*Pass:\*\*\s*(.*)/i, |
| /•\s*\*\*Pass:\*\*\s*(.*)/i, |
| /\*\s*\*\*Pass:\*\*\s*(.*)/i, |
| /-\s*\*\*Pass\*\*:\s*(.*)/i, |
| /•\s*\*\*Pass\*\*:\s*(.*)/i, |
| /\*\s*\*\*Pass\*\*:\s*(.*)/i, |
| /(?:•|-|\*)\s*\*\*Pass\*\*:?:\s*(.*)/i, |
| /(?:•|-|\*)\s*Pass:?:\s*(.*)/i, |
| /Pass:?:\s*(true|false|✅|❌|TRUE|FALSE)/im, |
| /(true|false|✅|❌|TRUE|FALSE)\s*pass/im |
| ]; |
| |
| 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); |
|
|
| |
| let errors: string[] = ['No errors reported.']; |
| let errorsMatch = null; |
| |
| |
| const errorPatterns = [ |
| /-\s*\*\*Errors:\*\*\s*([\s\S]*?)(?=\n-\s*\*\*|$)/, |
| /•\s*\*\*Errors:\*\*\s*([\s\S]*?)(?=\n•\s*\*\*|$)/, |
| /\*\s*\*\*Errors:\*\*\s*([\s\S]*?)(?=\n\*\s*\*\*|$)/, |
| /(?:•|-|\*)\s*\*\*Errors?\*\*:?:\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/, |
| /(?:•|-|\*)\s*Errors:?:\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/, |
| /Errors:?:\s*([\s\S]*?)(?=\n(?:•|-|\*|\*\*)|$)/m |
| ]; |
| |
| 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(']')) { |
| |
| try { |
| const parsed = JSON.parse(errorsBlock); |
| errors = Array.isArray(parsed) && parsed.length > 0 ? parsed : ['No errors reported.']; |
| } catch { |
| |
| errors = [errorsBlock.replace(/[\[\]]/g, '').trim()]; |
| } |
| } else { |
| |
| const lines = errorsBlock.split('\n').map(e => e.trim().replace(/^[-•\*]\s*/, '')).filter(Boolean); |
| errors = lines.length > 0 ? lines : ['No errors reported.']; |
| } |
| } |
|
|
| |
| |
| 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; |
| } |
| |
| |
| 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; |
| } |
| } |
|
|
| |
| let corrected = 'Content analysis not available.'; |
| let contentMatch = null; |
| |
| |
| const contentPatterns = [ |
| /-\s*\*\*Analysis:\*\*\s*([\s\S]*?)(?=\n-\s*\*\*|$)/, |
| /•\s*\*\*Analysis:\*\*\s*([\s\S]*?)(?=\n•\s*\*\*|$)/, |
| /\*\s*\*\*Analysis:\*\*\s*([\s\S]*?)(?=\n\*\s*\*\*|$)/, |
| /-\s*\*\*Corrected:\*\*\s*([\s\S]*?)(?=\n-\s*\*\*|$)/, |
| /•\s*\*\*Corrected:\*\*\s*([\s\S]*?)(?=\n•\s*\*\*|$)/, |
| /\*\s*\*\*Corrected:\*\*\s*([\s\S]*?)(?=\n\*\s*\*\*|$)/, |
| /(?:•|-|\*)\s*\*\*(?:Analysis|Corrected)\*\*:?:\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/, |
| /(?:•|-|\*)\s*(?:Analysis|Corrected):?:\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/, |
| /Analysis:?:\s*([\s\S]*?)(?=\n(?:•|-|\*|\*\*)|$)/m, |
| /Corrected:?:\s*([\s\S]*?)(?=\n(?:•|-|\*|\*\*)|$)/m |
| ]; |
| |
| for (const pattern of contentPatterns) { |
| contentMatch = sectionText.match(pattern); |
| if (contentMatch) { |
| corrected = contentMatch[1].trim(); |
| break; |
| } |
| } |
| |
| |
| if (!contentMatch || corrected.length < 10) { |
| |
| 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; |
| } |
| } |
| |
| |
| 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 }; |
| }; |
|
|
|
|
| |
| |
| |
| |
| |
| 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 { |
| |
| 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() || ''; |
| |
| |
| 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'; |
| |
| |
| 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; |
| |
| |
| 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 { |
| |
| 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(); |
| } |
| |
| |
| if (errorText.startsWith('[') && errorText.includes(']')) { |
| |
| try { |
| const parsedErrors = JSON.parse(errorText); |
| errors = Array.isArray(parsedErrors) ? parsedErrors : [errorText]; |
| } catch { |
| errors = [errorText]; |
| } |
| } else { |
| |
| errors = errorText.split('\n') |
| .map(e => e.trim().replace(/^- /, '')) |
| .filter(Boolean); |
| } |
| |
| if (errors.length === 0) { |
| errors = ['No errors reported.']; |
| } |
| } |
| } else { |
| errors = ['Errors not found.']; |
| } |
| |
| |
| 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')) { |
| |
| 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' }; |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| const parseSingleSectionFormat = (sectionText: string, defaultReport: DetailedQaReport): { detailedQaReport: DetailedQaReport, overallPass: boolean, overallGrade: string } => { |
| console.log('Parsing single-section format'); |
| |
| |
| 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 |
| }; |
| }; |
|
|
| |
| |
| |
| 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'; |
| }; |
|
|
| |
| |
| |
| 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; |
| }; |
|
|
| |
| |
| |
| const parseEnhancedSection = (sectionBlock: string): QaSectionResult => { |
| const baseSection = parseSection(sectionBlock); |
| |
| |
| 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); |
| |
| |
| 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; |
| |
| |
| 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 |
| }; |
| }; |
|
|
| |
| |
| |
| 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 = ''; |
| |
| |
| |
| const overallGradePatterns = [ |
| /Final\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, |
| /Overall\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, |
| /Total\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, |
| /Combined\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, |
| /Average\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, |
| /Mean\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, |
| /Composite\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, |
| /Final\s+Score:\s*(\d+(?:\.\d+)?)\/100/i, |
| /Overall\s+Score:\s*(\d+(?:\.\d+)?)\/100/i, |
| /Total\s+Score:\s*(\d+(?:\.\d+)?)\/100/i, |
| /Final\s+Rating:\s*(\d+(?:\.\d+)?)\/100/i, |
| /Overall\s+Rating:\s*(\d+(?:\.\d+)?)\/100/i, |
| /Final\s+Mark:\s*(\d+(?:\.\d+)?)\/100/i, |
| /Overall\s+Mark:\s*(\d+(?:\.\d+)?)\/100/i, |
| /Final\s+Points:\s*(\d+(?:\.\d+)?)\/100/i, |
| /Overall\s+Points:\s*(\d+(?:\.\d+)?)\/100/i, |
| /-?\s*\*\*Final\s+Grade:\*\*\s*(\d+(?:\.\d+)?)\/100/i, |
| /-?\s*\*\*Overall\s+Grade:\*\*\s*(\d+(?:\.\d+)?)\/100/i, |
| /-?\s*\*\*Total\s+Grade:\*\*\s*(\d+(?:\.\d+)?)\/100/i, |
| /-?\s*\*\*Final\s+Score:\*\*\s*(\d+(?:\.\d+)?)\/100/i, |
| /-?\s*\*\*Overall\s+Score:\*\*\s*(\d+(?:\.\d+)?)\/100/i, |
| /-?\s*\*\*Final\s+Rating:\*\*\s*(\d+(?:\.\d+)?)\/100/i, |
| /-?\s*\*\*Overall\s+Rating:\*\*\s*(\d+(?:\.\d+)?)\/100/i, |
| /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, |
| ]; |
| |
| 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; |
| } |
| } |
| |
| |
| |
| const overallPassPatterns = [ |
| /Overall\s+Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, |
| /Final\s+Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, |
| /Total\s+Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, |
| /Combined\s+Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, |
| /Average\s+Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, |
| /Overall\s+Status:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, |
| /Final\s+Status:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, |
| /Overall\s+Result:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, |
| /Final\s+Result:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, |
| /Overall\s+Assessment:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, |
| /Final\s+Assessment:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, |
| /-?\s*\*\*Overall\s+Pass:\*\*\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, |
| /-?\s*\*\*Final\s+Pass:\*\*\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, |
| /-?\s*\*\*Total\s+Pass:\*\*\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, |
| /-?\s*\*\*Overall\s+Status:\*\*\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, |
| /-?\s*\*\*Final\s+Status:\*\*\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, |
| /Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, |
| /Status:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, |
| /Result:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, |
| /Assessment:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, |
| ]; |
| |
| 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; |
| } |
| } |
| |
| |
| |
| const issuePatterns = [ |
| /Overall\s+Pass:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, |
| /Final\s+Pass:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, |
| /Total\s+Pass:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, |
| /Overall\s+Status:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, |
| /Final\s+Status:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, |
| /Pass:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, |
| /Status:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, |
| /Result:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, |
| /Assessment:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, |
| /-?\s*\*\*Overall\s+Pass:\*\*\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, |
| /-?\s*\*\*Final\s+Pass:\*\*\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, |
| /-?\s*\*\*Total\s+Pass:\*\*\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, |
| /-?\s*\*\*Overall\s+Status:\*\*\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, |
| /-?\s*\*\*Final\s+Status:\*\*\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, |
| /-?\s*\*\*Primary\s+Issue:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Main\s+Issue:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Key\s+Issue:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Issue:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Problem:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Concern:\*\*\s*([^\n]+)/i, |
| /Primary\s+Issue:\s*([^\n]+)/i, |
| /Main\s+Issue:\s*([^\n]+)/i, |
| /Key\s+Issue:\s*([^\n]+)/i, |
| /Issue:\s*([^\n]+)/i, |
| /Problem:\s*([^\n]+)/i, |
| /Concern:\s*([^\n]+)/i, |
| ]; |
| |
| 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; |
| } |
| } |
| |
| |
| |
| const breakdownPatterns = [ |
| /##\s+DETAILED\s+BREAKDOWN[^#]*/i, |
| /##\s+BREAKDOWN[^#]*/i, |
| /##\s+ANALYSIS[^#]*/i, |
| /##\s+ASSESSMENT[^#]*/i, |
| /##\s+EVALUATION[^#]*/i, |
| /##\s+REVIEW[^#]*/i, |
| /##\s+SUMMARY[^#]*/i, |
| /##\s+DETAILS[^#]*/i, |
| /##\s+EXPLANATION[^#]*/i, |
| /##\s+COMMENTS[^#]*/i, |
| /##\s+NOTES[^#]*/i, |
| /###\s+DETAILED\s+BREAKDOWN[^#]*/i, |
| /###\s+BREAKDOWN[^#]*/i, |
| /###\s+ANALYSIS[^#]*/i, |
| /###\s+ASSESSMENT[^#]*/i, |
| /###\s+EVALUATION[^#]*/i, |
| /###\s+REVIEW[^#]*/i, |
| /###\s+SUMMARY[^#]*/i, |
| /###\s+DETAILS[^#]*/i, |
| /###\s+EXPLANATION[^#]*/i, |
| /###\s+COMMENTS[^#]*/i, |
| /###\s+NOTES[^#]*/i, |
| ]; |
| |
| let breakdownMatch = null; |
| for (const pattern of breakdownPatterns) { |
| breakdownMatch = sectionBlock.match(pattern); |
| if (breakdownMatch) { |
| detailedAssessment = breakdownMatch[0]; |
| console.log('Found detailed breakdown'); |
| break; |
| } |
| } |
| |
| |
| |
| const lowerSection = sectionBlock.toLowerCase(); |
| |
| |
| 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'); |
| } |
| |
| |
| 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'); |
| } |
| |
| |
| |
| 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; |
| } |
| } |
| } |
| |
| |
| if (grade !== 'N/A' && !overallPassMatch) { |
| const gradeNum = parseFloat(grade.split('/')[0]); |
| pass = gradeNum >= 80; |
| console.log('Inferred overall pass status from grade:', pass); |
| } |
| |
| |
| |
| 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 |
| }; |
| }; |
|
|
| |
| |
| |
| 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'; |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| const parseNewQaReport = (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' }, |
| completeRawReport: qaText |
| }; |
|
|
| 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)); |
| |
| |
| |
| |
| const parsedData: Partial<DetailedQaReport> = {}; |
| const additionalSections: { [sectionName: string]: { content: string; type: 'assessment' | 'strengths' | 'recommendations' | 'explanations' | 'other'; } } = {}; |
| |
| |
| let sections: string[] = []; |
| |
| |
| const contentAfterOverallHeader = cleanedQaText.replace(/^(#\s*FINAL\s+QUALITY\s+ASSURANCE\s+EVALUATION|##\s*Section\s+Grades)\s*/i, '').trim(); |
| |
| |
| |
| const sectionHeaderPatterns = [ |
| |
| /(?=(?:##|###)\s*\*\*([^*]+)\*\*\s*-\s*GRADE:)/g, |
| |
| /(?=(?:##|###)\s*\*\*([^*]+):\s*(?:PASS|FAIL)\*\*\s*(?:✅|❌))/g, |
| |
| /(?=(?:##|###)\s*\*\*([^*]+)\s+GRADE:)/g, |
| |
| /(?=(?:##|###)\s*\*\*([^*]+)\*\*(?:\s*(?:✅|❌|PASS|FAIL))?)/g, |
| |
| /(?=(?:##|###)\s*[^#\n]*)/g, |
| ]; |
| |
| for (const pattern of sectionHeaderPatterns) { |
| sections = contentAfterOverallHeader.split(pattern).filter(Boolean); |
| if (sections.length > 1) { |
| console.log(`Using successful split pattern: ${pattern}`); |
| break; |
| } |
| } |
| |
| |
| 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)'); |
| } |
| |
| |
| sections = sections.filter(s => s.trim().length > 5); |
| |
| console.log(`Found ${sections.length} sections to parse`); |
| sections.forEach((section, index) => { |
| console.log(`Section ${index} preview:`, section.substring(0, 150)); |
| }); |
| |
| |
| 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 = ''; |
| let sectionData: QaSectionResult | null = null; |
| |
| |
| const headerForTypeCheck = header |
| .replace(/^(#+\s*|\*\*)/, '') |
| .replace(/[:*]/g, '') |
| .replace(/\s*-\s*grade:.*/, '') |
| .replace(/\s*(✅|❌|pass|fail).*/, '') |
| .trim(); |
|
|
| const words = headerForTypeCheck.split(/\s+/).filter(Boolean); |
|
|
| |
| 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 (!sectionType) { |
| if (headerForTypeCheck.startsWith('overall assessment') || headerForTypeCheck.startsWith('final assessment')) { |
| sectionType = 'overall'; |
| } |
| } |
| |
| |
| 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 { |
| |
| console.log('Identified as additional section'); |
| let displayName = headerRaw.replace(/^[#*\s-]+/g, '').trim(); |
| |
| additionalSections[displayName] = { |
| content: sectionBlock, |
| type: determineSectionType(sectionBlock) |
| }; |
| } |
| }); |
| |
| |
| if (Object.keys(parsedData).length === 0) { |
| console.log('No sections found, attempting full-text extraction'); |
| |
| |
| 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); |
| } |
| |
| |
| 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 |
| }; |
| |
| |
| 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 |
| }; |
| }; |
|
|
| |
| |
| |
| |
| 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)); |
| |
| |
| 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 = ''; |
| |
| |
| |
| |
| const headerPatterns = [ |
| /###\s*\*\*([^*]+)\*\*\s*(✅|❌)\s*(PASS|FAIL)/i, |
| /##\s*\*\*([^*]+):\s*(PASS|FAIL)\*\*\s*(✅|❌)/i, |
| /##\s*\*\*([^*]+)\*\*\s*(PASS|FAIL)\s*(✅|❌)/i, |
| /###\s*\*\*([^*]+):\s*(PASS|FAIL)\*\*\s*(✅|❌)/i, |
| /##\s*\*\*([^*]+)\*\*\s*(✅|❌)\s*(PASS|FAIL)/i, |
| /###\s*\*\*([^*]+)\*\*\s*(PASS|FAIL)/i, |
| /##\s*\*\*([^*]+)\*\*\s*(PASS|FAIL)/i, |
| /###\s*\*\*([^*]+):\s*(PASS|FAIL)/i, |
| /##\s*\*\*([^*]+):\s*(PASS|FAIL)/i, |
| /###\s*([^*]+)\s*(✅|❌)\s*(PASS|FAIL)/i, |
| /##\s*([^*]+)\s*(✅|❌)\s*(PASS|FAIL)/i, |
| /###\s*([^*]+):\s*(PASS|FAIL)\s*(✅|❌)/i, |
| /##\s*([^*]+):\s*(PASS|FAIL)\s*(✅|❌)/i, |
| /###\s*([^*]+)\s*(PASS|FAIL)/i, |
| /##\s*([^*]+)\s*(PASS|FAIL)/i, |
| /###\s*([^*]+):\s*(PASS|FAIL)/i, |
| /##\s*([^*]+):\s*(PASS|FAIL)/i, |
| ]; |
| |
| let headerMatch = null; |
| for (const pattern of headerPatterns) { |
| headerMatch = sectionBlock.match(pattern); |
| if (headerMatch) { |
| console.log('Found QA Guard header format:', headerMatch[0]); |
| |
| const passIndicators = [headerMatch[2], headerMatch[3]].filter(Boolean); |
| pass = passIndicators.some(indicator => |
| indicator === '✅' || indicator.toUpperCase() === 'PASS' |
| ); |
| break; |
| } |
| } |
| |
| |
| |
| const gradePatterns = [ |
| /-?\s*\*\*Grade:\*\*\s*(\d+(?:\.\d+)?)\/100/i, |
| /-?\s*\*\*Grade\*\*:\s*(\d+(?:\.\d+)?)\/100/i, |
| /-?\s*Grade:\s*(\d+(?:\.\d+)?)\/100/i, |
| /-?\s*Grade\s*:\s*(\d+(?:\.\d+)?)\/100/i, |
| /\*\*Grade:\*\*\s*(\d+(?:\.\d+)?)\/100/i, |
| /\*\*Grade\*\*:\s*(\d+(?:\.\d+)?)\/100/i, |
| /Grade:\s*(\d+(?:\.\d+)?)\/100/i, |
| /Grade\s*:\s*(\d+(?:\.\d+)?)\/100/i, |
| /-?\s*\*\*Score:\*\*\s*(\d+(?:\.\d+)?)\/100/i, |
| /-?\s*\*\*Rating:\*\*\s*(\d+(?:\.\d+)?)\/100/i, |
| /-?\s*\*\*Mark:\*\*\s*(\d+(?:\.\d+)?)\/100/i, |
| /-?\s*\*\*Points:\*\*\s*(\d+(?:\.\d+)?)\/100/i, |
| /-?\s*\*\*(\d+(?:\.\d+)?)\/100\s*points?\*\*/i, |
| /-?\s*(\d+(?:\.\d+)?)\/100\s*points?/i, |
| /-?\s*\*\*(\d+(?:\.\d+)?)\/100\*\*/i, |
| /-?\s*(\d+(?:\.\d+)?)\/100/i, |
| /\*\*(\d+(?:\.\d+)?)\/100\*\*/i, |
| /(\d+(?:\.\d+)?)\/100/i, |
| ]; |
| |
| let gradeMatch = null; |
| for (const pattern of gradePatterns) { |
| gradeMatch = sectionBlock.match(pattern); |
| if (gradeMatch) { |
| grade = `${gradeMatch[1]}/100`; |
| console.log('Found grade:', grade); |
| break; |
| } |
| } |
| |
| |
| |
| const compliancePatterns = [ |
| /-?\s*\*\*Compliance:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Status:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Result:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Assessment:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Evaluation:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Check:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Verification:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Review:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Analysis:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Summary:\*\*\s*([^\n]+)/i, |
| ]; |
| |
| let complianceMatch = null; |
| for (const pattern of compliancePatterns) { |
| complianceMatch = sectionBlock.match(pattern); |
| if (complianceMatch) { |
| const compliance = complianceMatch[1].trim(); |
| console.log('Found compliance:', compliance); |
| |
| |
| 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; |
| } |
| } |
| |
| |
| |
| const analysisPatterns = [ |
| /-?\s*\*\*Analysis:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, |
| /-?\s*\*\*Review:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, |
| /-?\s*\*\*Assessment:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, |
| /-?\s*\*\*Evaluation:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, |
| /-?\s*\*\*Summary:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, |
| /-?\s*\*\*Details:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, |
| /-?\s*\*\*Breakdown:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, |
| /-?\s*\*\*Explanation:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, |
| /-?\s*\*\*Comments:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, |
| /-?\s*\*\*Notes:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, |
| ]; |
| |
| 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; |
| } |
| } |
| |
| |
| |
| const errorPatterns = [ |
| /-?\s*\*\*Error:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Errors:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Issue:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Issues:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Problem:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Problems:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Violation:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Violations:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Warning:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Warnings:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Concern:\*\*\s*([^\n]+)/i, |
| /-?\s*\*\*Concerns:\*\*\s*([^\n]+)/i, |
| ]; |
| |
| 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; |
| } |
| } |
| |
| |
| 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(); |
| |
| const violationErrors = violationText.split(/\n\s*(?:-|\d+\.)\s*(?:❌|\⚠️)?\s*\*\*/g) |
| .map(line => { |
| |
| return line.replace(/MAJOR VIOLATION \([^)]+\):/, '') |
| .replace(/MINOR VIOLATION \([^)]+\):/, '') |
| .replace(/\*\*$/, '') |
| .trim(); |
| }) |
| .filter(line => line.length > 10); |
|
|
| if (violationErrors.length > 0) { |
| if (errors[0] === 'No errors reported.') { |
| errors = violationErrors; |
| } else { |
| |
| errors = [...violationErrors, ...errors]; |
| } |
| console.log('Found detailed violation errors:', violationErrors); |
| } |
| } |
| |
| |
| |
| const correctedPatterns = [ |
| /\*\*CORRECTED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, |
| /\*\*CORRECTED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, |
| /\*\*FIXED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, |
| /\*\*FIXED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, |
| /\*\*REVISED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, |
| /\*\*REVISED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, |
| /\*\*UPDATED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, |
| /\*\*UPDATED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, |
| /\*\*SUGGESTED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, |
| /\*\*SUGGESTED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, |
| /\*\*RECOMMENDED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, |
| /\*\*RECOMMENDED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, |
| /-?\s*\*\*Corrected:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, |
| /-?\s*\*\*Fixed:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, |
| /-?\s*\*\*Revised:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, |
| /-?\s*\*\*Updated:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, |
| /-?\s*\*\*Suggested:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, |
| /-?\s*\*\*Recommended:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, |
| ]; |
| |
| 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; |
| } |
| } |
| |
| |
| |
| if (detailedAssessment) { |
| |
| 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(); |
| |
| |
| 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'); |
| } |
| |
| |
| 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'); |
| } |
| } |
| |
| |
| |
| 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; |
| } |
| } |
| } |
| |
| |
| if (grade !== 'N/A' && !headerMatch && !complianceMatch) { |
| const gradeNum = parseFloat(grade.split('/')[0]); |
| pass = gradeNum >= 80; |
| console.log('Inferred pass status from grade:', pass); |
| } |
| |
| |
| |
| 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; |
| } |
| } |
| |
| |
| if (pass && errors[0] === 'No errors reported.') { |
| |
| } 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 |
| }; |
| }; |
|
|
|
|
| |
| |
| |
| |
| |
| 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(); |
| |
| if (RETRYABLE_STATUS_CODES.includes(response.status)) { |
| throw new WorkflowError(`RETRYABLE_HTTP_${response.status}`, `Temporary service issue (HTTP ${response.status}).`, 'network', responseText); |
| } |
| |
| 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 }); |
| } |
|
|
| |
| responseText = streamContent; |
|
|
| const lines = streamContent.trim().split('\n'); |
| const finishedLine = lines.find(l => /"event"\s*:\s*"workflow_finished"/.test(l) || l.includes('"workflow_finished"')) || ''; |
|
|
| if (!finishedLine) { |
| |
| 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 (isOverloaded) { |
| throw new WorkflowError('RETRYABLE_API_ERROR', 'Service overloaded. Retrying...', 'api', String(apiError)); |
| } |
| |
| 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); |
|
|
| |
| 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) { |
| |
| 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; |
| } |
| |
| |
| console.error(`Failed to process row. Attempt ${attempt}/${MAX_RETRIES}. Error: ${lastError.message}`); |
| if (responseText) { |
| |
| console.error('Problematic Response:', responseText); |
| } |
| break; |
| } |
| } |
|
|
| |
| |
| 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; |
| }; |
|
|