UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes CodeSearch (hybrid SQLite + pgvector), mem0/memgraph specialists, and all CFN skills.

518 lines (517 loc) 18.6 kB
/** * Agent Output Parser * Legacy stdout parser for backward compatibility * * @version 1.0.0 * @description Converts unstructured text output to structured JSON */ // ============================================================================ // Parser Class // ============================================================================ /** * AgentOutputParser: Legacy text output parser * Provides best-effort parsing of unstructured stdout */ export class AgentOutputParser { /** * Parse stdout text to structured agent output */ parse(text) { const errors = []; let confidence = 0.5; // Default confidence for legacy parsing try { // Attempt to parse as JSON first const jsonOutput = this.tryParseJSON(text); if (jsonOutput) { return { success: true, output: jsonOutput, errors: [], confidence: 0.95 }; } // Detect output type from text patterns const outputType = this.detectOutputType(text); // Parse based on detected type switch(outputType){ case 'loop3': return this.parseLoop3Text(text); case 'loop2': return this.parseLoop2Text(text); case 'product_owner': return this.parseProductOwnerText(text); default: return { success: false, errors: [ 'Unable to detect agent output type from text' ], confidence: 0.0 }; } } catch (error) { return { success: false, errors: [ `Parse error: ${error instanceof Error ? error.message : String(error)}` ], confidence: 0.0 }; } } /** * Attempt to parse text as JSON */ tryParseJSON(text) { try { // Try parsing the entire text const obj = JSON.parse(text); if (this.isValidAgentOutput(obj)) { return obj; } // Try extracting JSON from markdown code blocks const jsonMatch = text.match(/```json\s*\n([\s\S]*?)\n```/); if (jsonMatch) { const obj = JSON.parse(jsonMatch[1]); if (this.isValidAgentOutput(obj)) { return obj; } } return null; } catch { return null; } } /** * Basic validation for agent output structure */ isValidAgentOutput(obj) { if (typeof obj !== 'object' || obj === null) { return false; } const record = obj; return typeof record.success === 'boolean' && typeof record.confidence === 'number' && typeof record.output_type === 'string' && (record.output_type === 'loop3' || record.output_type === 'loop2' || record.output_type === 'product_owner'); } /** * Detect output type from text patterns */ detectOutputType(text) { const lower = text.toLowerCase(); // Check for Product Owner decision keywords if (/\b(PROCEED|ITERATE|ABORT)\b/.test(text) && /\b(decision|rationale)\b/i.test(lower)) { return 'product_owner'; } // Check for Loop 2 validation keywords if (/\b(approved|rejected|consensus|validation)\b/i.test(lower) && /\b(issues?|recommendations?)\b/i.test(lower)) { return 'loop2'; } // Check for Loop 3 implementation keywords if (/\b(deliverables?|implementation|created|modified)\b/i.test(lower) || /\b(files? (created|modified))\b/i.test(lower)) { return 'loop3'; } return 'unknown'; } /** * Parse Loop 3 implementer text output */ parseLoop3Text(text) { const output = { output_type: 'loop3', success: true, confidence: this.extractConfidence(text), iteration: this.extractIteration(text), deliverables: this.extractDeliverables(text), errors: this.extractErrors(text), metadata: { agent_type: this.extractAgentType(text) || 'unknown', timestamp: new Date().toISOString() } }; // Extract optional fields const summary = this.extractSummary(text); if (summary) { output.summary = summary; } const metrics = this.extractMetrics(text); if (Object.keys(metrics).length > 0) { output.metrics = metrics; } return { success: true, output, errors: [], confidence: 0.7 }; } /** * Parse Loop 2 validator text output */ parseLoop2Text(text) { const output = { output_type: 'loop2', success: true, confidence: this.extractConfidence(text), iteration: this.extractIteration(text), validation_type: this.extractValidationType(text), issues: this.extractIssues(text), recommendations: this.extractRecommendations(text), approved: this.extractApproval(text), errors: this.extractErrors(text), metadata: { agent_type: this.extractAgentType(text) || 'reviewer', timestamp: new Date().toISOString() } }; const summary = this.extractSummary(text); if (summary) { output.summary = summary; } return { success: true, output, errors: [], confidence: 0.7 }; } /** * Parse Product Owner text output */ parseProductOwnerText(text) { const decision = this.extractDecision(text); if (!decision) { return { success: false, errors: [ 'Unable to extract Product Owner decision' ], confidence: 0.0 }; } const output = { output_type: 'product_owner', success: true, confidence: this.extractConfidence(text), iteration: this.extractIteration(text), decision, rationale: this.extractRationale(text), deliverables_validated: this.extractDeliverablesValidated(text), next_action: this.extractNextAction(text), errors: this.extractErrors(text), metadata: { agent_type: 'product-owner', timestamp: new Date().toISOString() } }; const consensusScore = this.extractConsensusScore(text); if (consensusScore !== null) { output.consensus_score = consensusScore; } const gateScore = this.extractGateScore(text); if (gateScore !== null) { output.gate_score = gateScore; } return { success: true, output, errors: [], confidence: 0.75 }; } // ============================================================================ // Extraction Helper Methods // ============================================================================ /** * Extract confidence score from text */ extractConfidence(text) { // Look for explicit confidence score (fixed regex to match only valid decimals) const match = text.match(/confidence[:\s]+([0-9]+(?:\.[0-9]+)?)/i) || text.match(/confidence score[:\s]+([0-9]+(?:\.[0-9]+)?)/i); if (match) { const value = parseFloat(match[1]); // Handle both 0.0-1.0 and 0-100 formats if (value >= 0 && value <= 1) { return value; } if (value > 1 && value <= 100) { return value / 100; } } // Default confidence for parsed text return 0.7; } /** * Extract iteration number from text */ extractIteration(text) { const match = text.match(/iteration[:\s]+(\d+)/i) || text.match(/iteration (\d+)/i); if (match) { return parseInt(match[1], 10); } return 1; // Default to iteration 1 } /** * Extract agent type from text */ extractAgentType(text) { const match = text.match(/agent[_\s]?type[:\s]+([a-z-]+)/i) || text.match(/agent[:\s]+([a-z-]+)/i); return match ? match[1] : null; } /** * Extract summary from text */ extractSummary(text) { const match = text.match(/summary[:\s]+(.+?)(?:\n\n|\n[A-Z]|$)/is) || text.match(/## Summary\s*\n(.+?)(?:\n\n|\n#|$)/is); return match ? match[1].trim() : null; } /** * Extract deliverables from text */ extractDeliverables(text) { const deliverables = []; // Look for file listings const filePatterns = [ /(?:created|modified|deleted)[:\s]+([^\n]+)/gi, /[-*]\s+(?:created|modified|deleted)[:\s]+([^\n]+)/gi, /[-*]\s+`([^`]+)`\s+-\s+(created|modified|deleted)/gi ]; for (const pattern of filePatterns){ let match; while((match = pattern.exec(text)) !== null){ const path = match[1].trim().replace(/`/g, ''); const status = this.extractDeliverableStatus(match[0]); deliverables.push({ path, type: this.guessDeliverableType(path), status: status || 'created' }); } } return deliverables; } /** * Extract deliverable status from text */ extractDeliverableStatus(text) { const lower = text.toLowerCase(); if (lower.includes('created')) return 'created'; if (lower.includes('modified')) return 'modified'; if (lower.includes('deleted')) return 'deleted'; return null; } /** * Guess deliverable type from file path */ guessDeliverableType(path) { const lower = path.toLowerCase(); if (lower.match(/\.test\.|\.spec\.|test\/|tests\//)) return 'test'; if (lower.match(/\.md$|readme|docs?\//)) return 'documentation'; if (lower.match(/\.json$|\.ya?ml$|\.toml$|\.ini$/)) return 'config'; if (lower.match(/schema|\.graphql$/)) return 'schema'; if (lower.match(/\.sh$|\.bash$|scripts?\//)) return 'script'; if (lower.match(/\.ts$|\.js$|\.py$|src\//)) return 'implementation'; return 'other'; } /** * Extract metrics from text */ extractMetrics(text) { const metrics = {}; const patterns = [ { key: 'files_created', pattern: /(\d+)\s+files?\s+created/i }, { key: 'files_modified', pattern: /(\d+)\s+files?\s+modified/i }, { key: 'lines_of_code', pattern: /(\d+)\s+lines?\s+of\s+code/i }, { key: 'test_coverage', pattern: /(?:coverage|test coverage)[:\s]+([0-9]+(?:\.[0-9]+)?)%?/i }, { key: 'tests_passed', pattern: /(\d+)\s+tests?\s+passed/i }, { key: 'tests_failed', pattern: /(\d+)\s+tests?\s+failed/i } ]; for (const { key, pattern } of patterns){ const match = text.match(pattern); if (match) { let value = parseFloat(match[1]); // Convert percentage to 0.0-1.0 for test_coverage if (key === 'test_coverage' && value > 1) { value = value / 100; } metrics[key] = value; } } return metrics; } /** * Extract validation type from text */ extractValidationType(text) { const lower = text.toLowerCase(); if (lower.includes('security')) return 'security'; if (lower.includes('test')) return 'test'; if (lower.includes('architecture')) return 'architecture'; if (lower.includes('performance')) return 'performance'; if (lower.includes('compliance')) return 'compliance'; return 'review'; // Default } /** * Extract issues from text */ extractIssues(text) { const issues = []; // Look for issue listings const issuePatterns = [ /[-*]\s+\[([^\]]+)\]\s+([^\n]+)/g, /[-*]\s+(critical|high|medium|low|info)[:\s]+([^\n]+)/gi ]; for (const pattern of issuePatterns){ let match; while((match = pattern.exec(text)) !== null){ const severity = this.parseSeverity(match[1]); const message = match[2].trim(); issues.push({ severity, category: 'other', message }); } } return issues; } /** * Parse severity from text */ parseSeverity(text) { const lower = text.toLowerCase(); if (lower.includes('critical')) return 'critical'; if (lower.includes('high')) return 'high'; if (lower.includes('medium')) return 'medium'; if (lower.includes('low')) return 'low'; return 'info'; } /** * Extract recommendations from text */ extractRecommendations(text) { const recommendations = []; // Look for recommendation listings const patterns = [ /recommendation[s]?[:\s]+([^\n]+)/gi, /[-*]\s+(?:recommend|suggestion)[:\s]+([^\n]+)/gi ]; for (const pattern of patterns){ let match; while((match = pattern.exec(text)) !== null){ recommendations.push(match[1].trim()); } } return recommendations; } /** * Extract approval status from text */ extractApproval(text) { const lower = text.toLowerCase(); // Explicit approval/rejection if (/\bapproved\b/i.test(text)) return true; if (/\brejected\b/i.test(text)) return false; // Implicit approval from consensus (fixed regex to match only valid decimals) if (/consensus[:\s]+([0-9]+(?:\.[0-9]+)?)/i.test(text)) { const match = text.match(/consensus[:\s]+([0-9]+(?:\.[0-9]+)?)/i); if (match) { const score = parseFloat(match[1]); return score >= 0.9; // Default threshold } } return false; // Default to not approved } /** * Extract Product Owner decision */ extractDecision(text) { const match = text.match(/\b(PROCEED|ITERATE|ABORT)\b/); return match ? match[1] : null; } /** * Extract rationale from text */ extractRationale(text) { const match = text.match(/rationale[:\s]+(.+?)(?:\n\n|\n[A-Z]|$)/is) || text.match(/reason[:\s]+(.+?)(?:\n\n|\n[A-Z]|$)/is); return match ? match[1].trim() : 'No rationale provided in legacy output'; } /** * Extract deliverables validated status */ extractDeliverablesValidated(text) { const lower = text.toLowerCase(); return lower.includes('deliverables validated') || lower.includes('all deliverables') || lower.includes('deliverables complete'); } /** * Extract next action from text */ extractNextAction(text) { const match = text.match(/next[_\s]action[:\s]+([^\n]+)/i) || text.match(/action[:\s]+([^\n]+)/i); if (match) { return match[1].trim(); } // Infer from decision const decision = this.extractDecision(text); if (decision === 'PROCEED') return 'mark_task_complete'; if (decision === 'ITERATE') return 'start_next_iteration'; if (decision === 'ABORT') return 'abort_task'; return 'unknown'; } /** * Extract consensus score from text */ extractConsensusScore(text) { const match = text.match(/consensus[_\s]score[:\s]+([0-9]+(?:\.[0-9]+)?)/i) || text.match(/consensus[:\s]+([0-9]+(?:\.[0-9]+)?)/i); if (match) { const value = parseFloat(match[1]); return value >= 0 && value <= 1 ? value : value / 100; } return null; } /** * Extract gate score from text */ extractGateScore(text) { const match = text.match(/gate[_\s]score[:\s]+([0-9]+(?:\.[0-9]+)?)/i) || text.match(/gate[:\s]+([0-9]+(?:\.[0-9]+)?)/i); if (match) { const value = parseFloat(match[1]); return value >= 0 && value <= 1 ? value : value / 100; } return null; } /** * Extract errors from text */ extractErrors(text) { const errors = []; // Look for error listings const errorPatterns = [ /error[:\s]+\[([^\]]+)\]\s+([^\n]+)/gi, /\[ERROR\]\s+([^\n]+)/gi ]; for (const pattern of errorPatterns){ let match; while((match = pattern.exec(text)) !== null){ const code = match[1] || 'UNKNOWN_ERROR'; const message = match[2] || match[1]; errors.push({ code: code.trim(), message: message.trim() }); } } return errors; } } // ============================================================================ // Singleton Instance and Convenience Functions // ============================================================================ let parserInstance = null; /** * Get or create parser instance */ export function getParser() { if (!parserInstance) { parserInstance = new AgentOutputParser(); } return parserInstance; } /** * Parse text output to structured agent output */ export function parseAgentOutput(text) { return getParser().parse(text); } /** * Reset parser instance (useful for testing) */ export function resetParser() { parserInstance = null; } export default AgentOutputParser; //# sourceMappingURL=agent-output-parser.js.map