UNPKG

harmonycode

Version:

The AI collaboration framework that prevents echo chambers - Real-time collaboration with diversity enforcement

597 lines (507 loc) 17.5 kB
/** * Perspective Analyzer for Anti-Echo-Chamber System * Analyzes statements and decisions for perspective diversity */ import { PerspectiveProfile, PerspectiveMessage, EvidenceItem, PerspectiveScores } from './types'; export interface AnalysisResult { detectedPerspective: PerspectiveProfile; confidence: number; isEchoing: boolean; echoPatterns: EchoPattern[]; diversityContribution: number; evidenceQuality: number; recommendations: string[]; } export interface EchoPattern { type: 'PHRASE_REPETITION' | 'AGREEMENT_CASCADE' | 'GROUPTHINK' | 'BANDWAGON'; severity: 'LOW' | 'MEDIUM' | 'HIGH'; description: string; examples: string[]; } export interface StatementFeatures { sentiment: number; // -1 to 1 (negative to positive) certainty: number; // 0 to 1 innovation: number; // 0 to 1 (conservative to innovative) riskAwareness: number; // 0 to 1 evidenceBased: number; // 0 to 1 agreementSignals: string[]; disagreementSignals: string[]; keywords: string[]; } export class PerspectiveAnalyzer { private recentStatements: Map<string, string[]> = new Map(); private phraseFrequency: Map<string, number> = new Map(); private agreementPatterns!: RegExp[]; private disagreementPatterns!: RegExp[]; constructor() { this.initializePatterns(); } /** * Analyze a statement to detect perspective and echo patterns */ analyzeStatement( agentId: string, statement: string, context?: string[] ): AnalysisResult { // Extract features from statement const features = this.extractFeatures(statement); // Detect perspective based on features const detectedPerspective = this.detectPerspective(features); // Check for echo patterns const echoPatterns = this.detectEchoPatterns(statement, agentId, context); // Calculate diversity contribution const diversityContribution = this.calculateDiversityContribution( features, echoPatterns ); // Analyze evidence quality if present const evidenceQuality = this.analyzeEvidenceQuality(statement); // Generate recommendations const recommendations = this.generateRecommendations( features, echoPatterns, diversityContribution ); // Store statement for future echo detection this.recordStatement(agentId, statement); return { detectedPerspective: detectedPerspective.profile, confidence: detectedPerspective.confidence, isEchoing: echoPatterns.length > 0 && echoPatterns.some(p => p.severity !== 'LOW'), echoPatterns, diversityContribution, evidenceQuality, recommendations }; } /** * Compare two perspectives to measure difference */ comparePerspectives( perspective1: PerspectiveProfile, perspective2: PerspectiveProfile ): number { const scores1 = this.getPerspectiveScores(perspective1); const scores2 = this.getPerspectiveScores(perspective2); const differences = Object.keys(scores1).map(key => { const k = key as keyof PerspectiveScores; return Math.abs(scores1[k] - scores2[k]); }); return differences.reduce((sum, diff) => sum + diff, 0) / differences.length; } /** * Analyze evidence quality in a statement */ analyzeEvidenceQuality(statement: string): number { let score = 0; const lower = statement.toLowerCase(); // Check for evidence indicators const evidenceIndicators = [ { pattern: /stud(y|ies) show/i, weight: 0.2 }, { pattern: /data (indicate|show|suggest)/i, weight: 0.2 }, { pattern: /research (found|indicates)/i, weight: 0.2 }, { pattern: /\d+%/g, weight: 0.1 }, { pattern: /benchmark|metric|measurement/i, weight: 0.15 }, { pattern: /case study|example/i, weight: 0.1 }, { pattern: /source:|according to/i, weight: 0.1 } ]; evidenceIndicators.forEach(indicator => { if (indicator.pattern.test(lower)) { score += indicator.weight; } }); // Penalty for vague claims const vagueIndicators = [ /everyone knows/i, /obviously/i, /clearly/i, /it's well known/i, /common sense/i ]; vagueIndicators.forEach(pattern => { if (pattern.test(lower)) { score -= 0.1; } }); return Math.max(0, Math.min(1, score)); } /** * Extract linguistic features from statement */ private extractFeatures(statement: string): StatementFeatures { const lower = statement.toLowerCase(); const words = lower.split(/\s+/); // Sentiment analysis (simplified) const positiveWords = ['great', 'excellent', 'perfect', 'best', 'optimal']; const negativeWords = ['bad', 'poor', 'worst', 'problematic', 'issue']; const positiveCount = words.filter(w => positiveWords.includes(w)).length; const negativeCount = words.filter(w => negativeWords.includes(w)).length; const sentiment = (positiveCount - negativeCount) / Math.max(words.length * 0.1, 1); // Certainty detection const certaintyWords = ['definitely', 'certainly', 'absolutely', 'clearly', 'must']; const uncertaintyWords = ['maybe', 'perhaps', 'might', 'could', 'possibly']; const certaintyCount = words.filter(w => certaintyWords.includes(w)).length; const uncertaintyCount = words.filter(w => uncertaintyWords.includes(w)).length; const certainty = (certaintyCount - uncertaintyCount + 1) / 2; // Innovation vs conservative const innovativeWords = ['new', 'innovative', 'novel', 'creative', 'experimental']; const conservativeWords = ['traditional', 'proven', 'established', 'standard']; const innovationScore = words.filter(w => innovativeWords.includes(w)).length; const conservativeScore = words.filter(w => conservativeWords.includes(w)).length; const innovation = innovationScore / (innovationScore + conservativeScore + 1); // Risk awareness const riskWords = ['risk', 'danger', 'concern', 'issue', 'problem', 'challenge']; const riskAwareness = words.filter(w => riskWords.includes(w)).length / words.length; // Evidence-based score const evidenceWords = ['data', 'study', 'research', 'evidence', 'metric', 'measure']; const evidenceBased = words.filter(w => evidenceWords.includes(w)).length / words.length; // Agreement/disagreement signals const agreementSignals = this.findPatternMatches(statement, this.agreementPatterns); const disagreementSignals = this.findPatternMatches(statement, this.disagreementPatterns); // Extract keywords (non-common words) const commonWords = new Set(['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'from', 'up', 'about']); const keywords = words.filter(w => !commonWords.has(w) && w.length > 3); return { sentiment: Math.max(-1, Math.min(1, sentiment)), certainty: Math.max(0, Math.min(1, certainty)), innovation: Math.max(0, Math.min(1, innovation)), riskAwareness: Math.max(0, Math.min(1, riskAwareness)), evidenceBased: Math.max(0, Math.min(1, evidenceBased)), agreementSignals, disagreementSignals, keywords }; } /** * Detect perspective based on features */ private detectPerspective(features: StatementFeatures): { profile: PerspectiveProfile; confidence: number; } { const scores: Partial<Record<PerspectiveProfile, number>> = {}; // Score each perspective based on features if (features.sentiment > 0.5 && features.innovation > 0.5) { scores[PerspectiveProfile.OPTIMIST] = 0.8; scores[PerspectiveProfile.INNOVATOR] = 0.7; } if (features.certainty < 0.3 && features.evidenceBased > 0.5) { scores[PerspectiveProfile.SKEPTIC] = 0.8; scores[PerspectiveProfile.ANALYTICAL] = 0.7; } if (features.innovation < 0.3 && features.riskAwareness > 0.5) { scores[PerspectiveProfile.CONSERVATIVE] = 0.8; } if (Math.abs(features.sentiment) < 0.3 && features.evidenceBased > 0.3) { scores[PerspectiveProfile.PRAGMATIST] = 0.7; } if (features.innovation > 0.7) { scores[PerspectiveProfile.CREATIVE] = 0.6; } // Find highest scoring perspective let bestProfile = PerspectiveProfile.PRAGMATIST; let bestScore = 0; Object.entries(scores).forEach(([profile, score]) => { if (score > bestScore) { bestScore = score; bestProfile = profile as PerspectiveProfile; } }); return { profile: bestProfile, confidence: bestScore || 0.5 }; } /** * Detect echo chamber patterns */ private detectEchoPatterns( statement: string, agentId: string, context?: string[] ): EchoPattern[] { const patterns: EchoPattern[] = []; // Check for phrase repetition const phraseRepetition = this.detectPhraseRepetition(statement); if (phraseRepetition) { patterns.push(phraseRepetition); } // Check for agreement cascade if (this.detectAgreementCascade(statement, context)) { patterns.push({ type: 'AGREEMENT_CASCADE', severity: 'HIGH', description: 'Sequential agreement without new insights', examples: context?.slice(-3) || [] }); } // Check for groupthink indicators const groupthink = this.detectGroupthink(statement, context); if (groupthink) { patterns.push(groupthink); } // Check for bandwagon effect if (this.detectBandwagon(statement)) { patterns.push({ type: 'BANDWAGON', severity: 'MEDIUM', description: 'Following majority opinion without critical evaluation', examples: [statement] }); } return patterns; } /** * Detect repeated phrases across agents */ private detectPhraseRepetition(statement: string): EchoPattern | null { const phrases = this.extractPhrases(statement); let repeatedPhrases: string[] = []; phrases.forEach(phrase => { const count = this.phraseFrequency.get(phrase) || 0; this.phraseFrequency.set(phrase, count + 1); if (count > 2) { repeatedPhrases.push(phrase); } }); if (repeatedPhrases.length > 0) { return { type: 'PHRASE_REPETITION', severity: repeatedPhrases.length > 2 ? 'HIGH' : 'MEDIUM', description: 'Multiple agents using identical phrases', examples: repeatedPhrases }; } return null; } /** * Detect agreement cascade pattern */ private detectAgreementCascade(statement: string, context?: string[]): boolean { if (!context || context.length < 3) return false; const recentAgreements = context.filter(s => this.agreementPatterns.some(p => p.test(s)) ).length; const hasAgreement = this.agreementPatterns.some(p => p.test(statement)); return hasAgreement && recentAgreements >= 2; } /** * Detect groupthink indicators */ private detectGroupthink(statement: string, context?: string[]): EchoPattern | null { const groupthinkPhrases = [ /we all agree/i, /consensus is clear/i, /everyone thinks/i, /no need to discuss further/i, /the obvious choice/i ]; const matches = groupthinkPhrases.filter(p => p.test(statement)); if (matches.length > 0) { return { type: 'GROUPTHINK', severity: 'HIGH', description: 'Premature consensus without exploring alternatives', examples: [statement] }; } return null; } /** * Detect bandwagon effect */ private detectBandwagon(statement: string): boolean { const bandwagonPhrases = [ /since everyone/i, /like others said/i, /following the majority/i, /as mentioned before/i, /adding to what.*said/i ]; return bandwagonPhrases.some(p => p.test(statement)); } /** * Calculate how much this statement contributes to diversity */ private calculateDiversityContribution( features: StatementFeatures, echoPatterns: EchoPattern[] ): number { let score = 0.5; // Baseline // Boost for disagreement if (features.disagreementSignals.length > 0) { score += 0.3; } // Boost for evidence score += features.evidenceBased * 0.2; // Penalty for echo patterns echoPatterns.forEach(pattern => { if (pattern.severity === 'HIGH') score -= 0.3; else if (pattern.severity === 'MEDIUM') score -= 0.2; else score -= 0.1; }); // Boost for unique keywords const uniqueKeywords = features.keywords.filter(k => (this.phraseFrequency.get(k) || 0) < 2 ); score += Math.min(0.2, uniqueKeywords.length * 0.05); return Math.max(0, Math.min(1, score)); } /** * Generate recommendations for improving diversity */ private generateRecommendations( features: StatementFeatures, echoPatterns: EchoPattern[], diversityContribution: number ): string[] { const recommendations: string[] = []; if (echoPatterns.some(p => p.type === 'AGREEMENT_CASCADE')) { recommendations.push('Provide a contrasting viewpoint or play devil\'s advocate'); } if (features.evidenceBased < 0.3) { recommendations.push('Support claims with data or case studies'); } if (features.innovation < 0.3 && features.disagreementSignals.length === 0) { recommendations.push('Consider proposing an alternative approach'); } if (diversityContribution < 0.3) { recommendations.push('Challenge an assumption or identify potential risks'); } if (features.certainty > 0.8) { recommendations.push('Acknowledge uncertainties or edge cases'); } return recommendations; } // Helper methods private initializePatterns(): void { this.agreementPatterns = [ /i agree/i, /exactly/i, /that's right/i, /absolutely/i, /correct/i, /precisely/i, /well said/i, /couldn't agree more/i ]; this.disagreementPatterns = [ /i disagree/i, /however/i, /on the other hand/i, /alternatively/i, /but/i, /actually/i, /that's not quite/i, /i would argue/i ]; } private findPatternMatches(text: string, patterns: RegExp[]): string[] { const matches: string[] = []; patterns.forEach(pattern => { const match = text.match(pattern); if (match) { matches.push(match[0]); } }); return matches; } private extractPhrases(statement: string): string[] { const words = statement.toLowerCase().split(/\s+/); const phrases: string[] = []; // Extract 2-3 word phrases for (let i = 0; i < words.length - 1; i++) { phrases.push(words.slice(i, i + 2).join(' ')); if (i < words.length - 2) { phrases.push(words.slice(i, i + 3).join(' ')); } } return phrases; } private recordStatement(agentId: string, statement: string): void { const agentStatements = this.recentStatements.get(agentId) || []; agentStatements.push(statement); // Keep only recent statements if (agentStatements.length > 20) { agentStatements.shift(); } this.recentStatements.set(agentId, agentStatements); } private getPerspectiveScores(profile: PerspectiveProfile): PerspectiveScores { // This would normally come from a configuration or the diversity tracker // For now, returning characteristic scores for each profile const scores: Record<PerspectiveProfile, PerspectiveScores> = { [PerspectiveProfile.OPTIMIST]: { riskTolerance: 0.8, innovationBias: 0.7, evidencePreference: 0.4, decisionSpeed: 0.7, conflictTolerance: 0.3 }, [PerspectiveProfile.SKEPTIC]: { riskTolerance: 0.2, innovationBias: 0.3, evidencePreference: 0.9, decisionSpeed: 0.3, conflictTolerance: 0.8 }, [PerspectiveProfile.PRAGMATIST]: { riskTolerance: 0.5, innovationBias: 0.5, evidencePreference: 0.7, decisionSpeed: 0.6, conflictTolerance: 0.5 }, [PerspectiveProfile.INNOVATOR]: { riskTolerance: 0.9, innovationBias: 0.9, evidencePreference: 0.3, decisionSpeed: 0.8, conflictTolerance: 0.6 }, [PerspectiveProfile.CONSERVATIVE]: { riskTolerance: 0.1, innovationBias: 0.2, evidencePreference: 0.8, decisionSpeed: 0.2, conflictTolerance: 0.2 }, [PerspectiveProfile.ANALYTICAL]: { riskTolerance: 0.4, innovationBias: 0.5, evidencePreference: 0.95, decisionSpeed: 0.2, conflictTolerance: 0.6 }, [PerspectiveProfile.CREATIVE]: { riskTolerance: 0.7, innovationBias: 0.85, evidencePreference: 0.2, decisionSpeed: 0.8, conflictTolerance: 0.7 }, [PerspectiveProfile.DETAIL_ORIENTED]: { riskTolerance: 0.3, innovationBias: 0.4, evidencePreference: 0.85, decisionSpeed: 0.1, conflictTolerance: 0.4 }, [PerspectiveProfile.BIG_PICTURE]: { riskTolerance: 0.6, innovationBias: 0.7, evidencePreference: 0.4, decisionSpeed: 0.9, conflictTolerance: 0.5 } }; return scores[profile]; } }