UNPKG

claude-buddy

Version:

Your friendly AI development companion for Claude Code - supercharge Claude Code with intelligent workflows and safety features

822 lines (714 loc) 25.4 kB
/** * Learning Engine for Persona System * * Implements context memory, pattern recognition, and adaptive improvements * for the persona activation and collaboration system. */ import { promises as fs } from 'fs'; import path from 'path'; import type { PersonaFeedback, LearningAnalytics, FeedbackRecord } from '../types/personas.js'; import type { LearningRecommendations, LearningAdaptation, InputContext } from '../types/context.js'; /** * Activation data for learning system * @category Learning Engine */ export interface ActivationData { userInput: string; command?: string | undefined; personas: string[]; collaborationPattern?: string | undefined; confidence?: number | number[] | undefined; activationType: 'manual' | 'automatic'; projectType?: string | undefined; filePatterns?: string[] | undefined; learningRecommendations?: LearningRecommendations | undefined; } interface InteractionRecord { sessionId: string; timestamp: number; type: 'activation'; context: CapturedContext; userInput: string; command?: string; personas: string[]; collaborationPattern?: string; confidence?: number | number[]; activationType: 'manual' | 'automatic'; projectType?: string; filePatterns?: string[]; learningRecommendations?: LearningRecommendations; } interface CapturedContext { projectType?: string | undefined; filePatterns?: Record<string, number> | undefined; commandType?: string | undefined; userInput: string; activatedPersonas: string[]; collaborationPattern?: string | undefined; confidence?: number | number[] | undefined; } interface SessionPattern { count: number; successCount: number; contexts: CapturedContext[]; lastUsed: number; confidence: number; } interface SessionMemory { interactions: (InteractionRecord | FeedbackRecord)[]; patterns: Map<string, SessionPattern>; feedback: FeedbackRecord[]; adaptations: LearningAdaptation[]; contextHistory: CapturedContext[]; } interface SuccessfulPattern { pattern: string; context: CapturedContext; personas: string[]; rating: number; comments: string; timestamp: number; reinforcementCount: number; averageRating: number; lastReinforced?: number; } interface FailedPattern { pattern: string; context: CapturedContext; personas: string[]; rating: number; issues: string; timestamp: number; } interface AdaptationHistory { sessionId: string; duration: number; interactions: number; feedback: number; patterns: number; timestamp: number; } interface PersistentMemory { successfulPatterns: SuccessfulPattern[]; failedPatterns: FailedPattern[]; userPreferences: Record<string, unknown>; projectPatterns: Record<string, unknown>; adaptationHistory: AdaptationHistory[]; performanceMetrics: Record<string, unknown>; } interface LearningConfig { maxSessionInteractions: number; maxPersistentPatterns: number; learningRate: number; confidenceThreshold: number; patternExpiryDays: number; adaptationEnabled: boolean; } interface PatternAdaptation { type: 'add_persona'; persona: string; reason: string; } interface SystemRecommendation { type: 'improvement' | 'diversity'; message: string; priority: 'low' | 'medium' | 'high'; } interface TopPattern { pattern: string; usage: number; rating: number; personas: string[]; } class PersonaLearningEngine { private dataDir: string; private memoryFile: string; private analyticsFile: string; private sessionMemory: SessionMemory; private persistentMemory: PersistentMemory; private config: LearningConfig; private sessionId!: string; private sessionStartTime!: number; constructor(dataDir?: string) { this.dataDir = dataDir || path.join(__dirname, '..', '.claude-buddy'); this.memoryFile = path.join(this.dataDir, 'persona-memory.json'); this.analyticsFile = path.join(this.dataDir, 'persona-analytics.json'); // In-memory storage for current session this.sessionMemory = { interactions: [], patterns: new Map(), feedback: [], adaptations: [], contextHistory: [] }; // Persistent learning data this.persistentMemory = { successfulPatterns: [], failedPatterns: [], userPreferences: {}, projectPatterns: {}, adaptationHistory: [], performanceMetrics: {} }; // Learning configuration this.config = { maxSessionInteractions: 100, maxPersistentPatterns: 500, learningRate: 0.1, confidenceThreshold: 0.8, patternExpiryDays: 30, adaptationEnabled: true }; } /** * Initialize learning engine and load persistent data */ async initialize(): Promise<boolean> { try { // Ensure data directory exists await fs.mkdir(this.dataDir, { recursive: true }); // Load persistent memory await this.loadPersistentMemory(); // Initialize session tracking this.sessionId = this.generateSessionId(); this.sessionStartTime = Date.now(); console.log('Learning engine initialized successfully'); return true; } catch (error) { console.error('Failed to initialize learning engine:', error); return false; } } /** * Load persistent memory from storage */ private async loadPersistentMemory(): Promise<void> { try { const memoryContent = await fs.readFile(this.memoryFile, 'utf8'); this.persistentMemory = JSON.parse(memoryContent); // Clean expired patterns this.cleanExpiredPatterns(); } catch (error) { // File doesn't exist yet, use defaults console.log('No existing memory file found, starting with empty memory'); } } /** * Save persistent memory to storage */ private async savePersistentMemory(): Promise<void> { try { const memoryData = { ...this.persistentMemory, lastUpdated: Date.now(), version: '1.0.0' }; await fs.writeFile(this.memoryFile, JSON.stringify(memoryData, null, 2)); } catch (error) { console.error('Failed to save persistent memory:', error); } } /** * Record persona activation and context for learning improvement. * * This method captures activation patterns, user context, and persona * selections to improve future recommendations through machine learning. * * @param activationData - Complete activation context and results * * @example * ```typescript * learningEngine.recordActivation({ * userInput: "Review security vulnerabilities", * personas: ["security", "backend"], * activationType: "automatic", * confidence: [0.85, 0.72], * projectType: "web-app", * filePatterns: ["*.ts", "*.js"] * }); * ``` * * @category Learning Engine * @public */ recordActivation(activationData: ActivationData): void { const interaction: InteractionRecord = { sessionId: this.sessionId, timestamp: Date.now(), type: 'activation', context: this.captureContext(activationData), ...activationData }; this.sessionMemory.interactions.push(interaction); // Learn from activation patterns this.updateActivationPatterns(interaction); // Trim session memory if needed if (this.sessionMemory.interactions.length > this.config.maxSessionInteractions) { this.sessionMemory.interactions = this.sessionMemory.interactions.slice(-this.config.maxSessionInteractions); } } /** * Record user feedback for learning improvement */ recordFeedback(feedbackData: PersonaFeedback): void { const feedback: any = { sessionId: this.sessionId, timestamp: Date.now(), type: 'feedback', personas: feedbackData.personas, rating: feedbackData.rating, comments: feedbackData.comments || '', context: feedbackData.context || {} }; this.sessionMemory.feedback.push(feedback); this.sessionMemory.interactions.push(feedback); // Learn from feedback patterns this.learnFromFeedback(feedback); // Update success/failure patterns this.updatePatternSuccessRates(feedback); } /** * Capture relevant context for learning */ private captureContext(activationData: ActivationData): CapturedContext { return { projectType: activationData.projectType, filePatterns: this.normalizeFilePatterns(activationData.filePatterns), commandType: activationData.command, userInput: this.sanitizeUserInput(activationData.userInput), activatedPersonas: activationData.personas || [], collaborationPattern: activationData.collaborationPattern, confidence: activationData.confidence }; } /** * Normalize file patterns for consistent processing */ private normalizeFilePatterns(filePatterns?: string[]): Record<string, number> { if (!filePatterns) return {}; const normalized: Record<string, number> = { frontend: 0, backend: 0, test: 0, config: 0, docs: 0 }; for (const pattern of filePatterns) { if (/\.(jsx?|tsx?|vue|svelte)$/i.test(pattern)) normalized.frontend++; if (/\.(py|java|php|rb|go|rs)$/i.test(pattern)) normalized.backend++; if (/\.(test|spec)\./i.test(pattern)) normalized.test++; if (/\.(json|yaml|yml|toml|ini)$/i.test(pattern)) normalized.config++; if (/\.(md|txt|rst)$/i.test(pattern)) normalized.docs++; } return normalized; } /** * Update activation patterns based on successful interactions */ private updateActivationPatterns(interaction: InteractionRecord): void { const patternKey = this.generatePatternKey(interaction); if (!this.sessionMemory.patterns.has(patternKey)) { this.sessionMemory.patterns.set(patternKey, { count: 0, successCount: 0, contexts: [], lastUsed: Date.now(), confidence: 0 }); } const pattern = this.sessionMemory.patterns.get(patternKey)!; pattern.count++; pattern.lastUsed = Date.now(); pattern.contexts.push(interaction.context); // Limit context history per pattern if (pattern.contexts.length > 10) { pattern.contexts = pattern.contexts.slice(-10); } } /** * Learn from user feedback to improve future activations */ private learnFromFeedback(feedback: FeedbackRecord): void { const relatedInteractions = this.findRelatedInteractions(feedback); for (const interaction of relatedInteractions) { if (interaction.type === 'activation') { const patternKey = this.generatePatternKey(interaction); const pattern = this.sessionMemory.patterns.get(patternKey); if (pattern) { // Update success rate based on feedback if (feedback.rating >= 4) { pattern.successCount++; this.reinforceSuccessfulPattern(patternKey, interaction, feedback); } else if (feedback.rating <= 2) { this.recordFailedPattern(patternKey, interaction, feedback); } // Update confidence score pattern.confidence = pattern.successCount / pattern.count; } } } } /** * Find interactions related to feedback */ private findRelatedInteractions(feedback: FeedbackRecord): InteractionRecord[] { const timeWindow = 5 * 60 * 1000; // 5 minutes const feedbackTime = feedback.timestamp; return this.sessionMemory.interactions.filter((interaction): interaction is InteractionRecord => { return Math.abs(interaction.timestamp - feedbackTime) <= timeWindow && interaction.type === 'activation'; }); } /** * Reinforce successful activation patterns */ private reinforceSuccessfulPattern(patternKey: string, interaction: InteractionRecord, feedback: FeedbackRecord): void { const successPattern: SuccessfulPattern = { pattern: patternKey, context: interaction.context, personas: interaction.personas, rating: feedback.rating, comments: feedback.comments || '', timestamp: Date.now(), reinforcementCount: 1, averageRating: feedback.rating }; // Check if pattern already exists const existingPattern = this.persistentMemory.successfulPatterns.find( p => p.pattern === patternKey ); if (existingPattern) { existingPattern.reinforcementCount++; existingPattern.lastReinforced = Date.now(); existingPattern.averageRating = (existingPattern.averageRating + feedback.rating) / 2; } else { this.persistentMemory.successfulPatterns.push(successPattern); } // Trim if we have too many patterns if (this.persistentMemory.successfulPatterns.length > this.config.maxPersistentPatterns) { this.persistentMemory.successfulPatterns.sort((a, b) => b.reinforcementCount - a.reinforcementCount); this.persistentMemory.successfulPatterns = this.persistentMemory.successfulPatterns.slice(0, this.config.maxPersistentPatterns); } } /** * Record failed activation patterns to avoid in future */ private recordFailedPattern(patternKey: string, interaction: InteractionRecord, feedback: FeedbackRecord): void { const failedPattern: FailedPattern = { pattern: patternKey, context: interaction.context, personas: interaction.personas, rating: feedback.rating, issues: feedback.comments || '', timestamp: Date.now() }; this.persistentMemory.failedPatterns.push(failedPattern); // Trim if we have too many failed patterns if (this.persistentMemory.failedPatterns.length > this.config.maxPersistentPatterns / 2) { this.persistentMemory.failedPatterns = this.persistentMemory.failedPatterns.slice(-250); } } /** * Generate pattern key for learning */ private generatePatternKey(interaction: InteractionRecord): string { const context = interaction.context; const key = [ context.projectType || 'unknown', context.commandType || 'unknown', (context.activatedPersonas || []).sort().join(','), this.categorizeFilePatterns(context.filePatterns) ].join('|'); return key; } /** * Categorize file patterns for pattern matching */ private categorizeFilePatterns(filePatterns?: Record<string, number>): string { if (!filePatterns) return 'none'; const categories: string[] = []; if (filePatterns.frontend > 10) categories.push('frontend'); if (filePatterns.backend > 10) categories.push('backend'); if (filePatterns.test > 5) categories.push('test'); if (filePatterns.config > 5) categories.push('config'); if (filePatterns.docs > 3) categories.push('docs'); return categories.length > 0 ? categories.sort().join(',') : 'general'; } /** * Get activation recommendations based on learned patterns */ getActivationRecommendations(context: Partial<InputContext>): LearningRecommendations { const recommendations: LearningRecommendations = { personas: [], confidence: 0, reasoning: [], patterns: [], adaptations: [] }; // Find matching successful patterns const matchingPatterns = this.findMatchingPatterns(context); if (matchingPatterns.length > 0) { // Sort by reinforcement count and confidence matchingPatterns.sort((a, b) => { return (b.reinforcementCount * b.averageRating) - (a.reinforcementCount * a.averageRating); }); const bestPattern = matchingPatterns[0]; recommendations.personas = bestPattern.personas || []; recommendations.confidence = bestPattern.averageRating / 5; // Convert to 0-1 scale recommendations.reasoning.push(`Learned pattern: ${bestPattern.reinforcementCount} successful uses`); recommendations.patterns.push(bestPattern.pattern); // Check for adaptations const adaptations = this.suggestAdaptations(context, bestPattern); recommendations.adaptations = adaptations; } // Check for anti-patterns (failed patterns to avoid) const antiPatterns = this.findAntiPatterns(context); if (antiPatterns.length > 0) { recommendations.reasoning.push(`Avoiding ${antiPatterns.length} known unsuccessful patterns`); } return recommendations; } /** * Find successful patterns matching current context */ private findMatchingPatterns(context: Partial<InputContext>): SuccessfulPattern[] { return this.persistentMemory.successfulPatterns.filter(pattern => { return this.contextMatches(pattern.context, context); }); } /** * Find anti-patterns (failed patterns) to avoid */ private findAntiPatterns(context: Partial<InputContext>): FailedPattern[] { return this.persistentMemory.failedPatterns.filter(pattern => { return this.contextMatches(pattern.context, context); }); } /** * Check if contexts match for pattern recognition */ private contextMatches(patternContext: CapturedContext, currentContext: Partial<InputContext>): boolean { // Simple context matching - could be enhanced with fuzzy matching const projectTypeMatch = patternContext.projectType === currentContext.projectType; const commandMatch = patternContext.commandType === currentContext.command; // File pattern similarity const normalizedCurrentFiles = this.normalizeFilePatterns(currentContext.files); const filePatternSimilarity = this.calculateFilePatternSimilarity( patternContext.filePatterns, normalizedCurrentFiles ); return projectTypeMatch && commandMatch && filePatternSimilarity > 0.7; } /** * Calculate similarity between file patterns */ private calculateFilePatternSimilarity(pattern1?: Record<string, number>, pattern2?: Record<string, number>): number { if (!pattern1 || !pattern2) return 0; const keys = ['frontend', 'backend', 'test', 'config', 'docs']; let similarity = 0; for (const key of keys) { const val1 = pattern1[key] || 0; const val2 = pattern2[key] || 0; const maxVal = Math.max(val1, val2, 1); // Avoid division by zero similarity += 1 - Math.abs(val1 - val2) / maxVal; } return similarity / keys.length; } /** * Suggest adaptations based on learned patterns and current context */ private suggestAdaptations(context: Partial<InputContext>, bestPattern: SuccessfulPattern): LearningAdaptation[] { const adaptations: LearningAdaptation[] = []; // Suggest additional personas based on context differences const normalizedCurrentFiles = this.normalizeFilePatterns(context.files); if (normalizedCurrentFiles && bestPattern.context.filePatterns) { const contextFiles = normalizedCurrentFiles; const patternFiles = bestPattern.context.filePatterns; // Suggest frontend persona if more frontend files than pattern if (contextFiles.frontend > patternFiles.frontend + 5) { adaptations.push({ type: 'persona_selection', reason: 'More frontend files detected than in learned pattern', impact: 0.3, confidence: 0.7 }); } // Suggest security persona if security-related files detected if (contextFiles.config > patternFiles.config + 3) { adaptations.push({ type: 'persona_selection', reason: 'Additional configuration files may need security review', impact: 0.4, confidence: 0.8 }); } } return adaptations; } /** * Clean expired patterns from memory */ private cleanExpiredPatterns(): void { const expiryTime = Date.now() - (this.config.patternExpiryDays * 24 * 60 * 60 * 1000); this.persistentMemory.successfulPatterns = this.persistentMemory.successfulPatterns.filter( pattern => pattern.timestamp > expiryTime ); this.persistentMemory.failedPatterns = this.persistentMemory.failedPatterns.filter( pattern => pattern.timestamp > expiryTime ); } /** * Update pattern success rates based on feedback */ private updatePatternSuccessRates(feedback: FeedbackRecord): void { // This is handled in learnFromFeedback, but could be expanded // for more sophisticated success rate calculations } /** * Sanitize user input for safe storage */ private sanitizeUserInput(userInput: string): string { if (!userInput) return ''; // Remove sensitive information patterns return userInput .replace(/--?[a-zA-Z-]+=["']?[^"'\s]*["']?/g, '[FLAG]') // Remove flags with values .replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '[EMAIL]') // Remove emails .replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, '[IP]') // Remove IP addresses .substring(0, 200); // Limit length } /** * Generate unique session ID */ private generateSessionId(): string { return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Get comprehensive learning analytics and performance insights. * * Provides detailed metrics about learning system performance, including * session statistics, persistent learning data, effectiveness scores, * top performing patterns, and system optimization recommendations. * * @returns Complete learning analytics with performance metrics * * @example * ```typescript * const analytics = learningEngine.getAnalytics(); * * console.log('Session interactions:', analytics.sessionStats.interactions); * console.log('Learning effectiveness:', analytics.learningEffectiveness); * console.log('Top patterns:', analytics.topPatterns.slice(0, 3)); * * // Act on recommendations * analytics.recommendations.forEach(rec => { * if (rec.priority === 'high') { * console.log('Priority recommendation:', rec.message); * } * }); * ``` * * @category Learning Engine * @public */ getAnalytics(): LearningAnalytics { return { sessionStats: { interactions: this.sessionMemory.interactions.length, feedback: this.sessionMemory.feedback.length, patterns: this.sessionMemory.patterns.size, sessionDuration: Date.now() - this.sessionStartTime }, persistentStats: { successfulPatterns: this.persistentMemory.successfulPatterns.length, failedPatterns: this.persistentMemory.failedPatterns.length, totalLearningEvents: this.persistentMemory.adaptationHistory.length }, learningEffectiveness: this.calculateLearningEffectiveness(), topPatterns: this.getTopPatterns(), recommendations: this.getSystemRecommendations() }; } /** * Calculate learning effectiveness metrics */ private calculateLearningEffectiveness(): number { const recentFeedback = this.sessionMemory.feedback.filter( f => f.timestamp > Date.now() - (7 * 24 * 60 * 60 * 1000) // Last 7 days ); if (recentFeedback.length === 0) return 0; const averageRating = recentFeedback.reduce((sum, f) => sum + f.rating, 0) / recentFeedback.length; return averageRating / 5; // Convert to 0-1 scale } /** * Get top performing patterns */ private getTopPatterns(): TopPattern[] { return this.persistentMemory.successfulPatterns .sort((a, b) => (b.reinforcementCount * b.averageRating) - (a.reinforcementCount * a.averageRating)) .slice(0, 10) .map(pattern => ({ pattern: pattern.pattern, usage: pattern.reinforcementCount, rating: pattern.averageRating, personas: pattern.personas })); } /** * Get system recommendations for improvement */ private getSystemRecommendations(): SystemRecommendation[] { const recommendations: SystemRecommendation[] = []; // Check if learning is effective const effectiveness = this.calculateLearningEffectiveness(); if (effectiveness < 0.6) { recommendations.push({ type: 'improvement', message: 'Learning effectiveness is low. Consider providing more feedback.', priority: 'medium' }); } // Check for pattern diversity const uniquePatterns = new Set(this.persistentMemory.successfulPatterns.map(p => p.pattern)); if (uniquePatterns.size < 5) { recommendations.push({ type: 'diversity', message: 'Limited pattern diversity detected. Try using different commands and contexts.', priority: 'low' }); } return recommendations; } /** * End session and save learning data */ async endSession(): Promise<void> { // Save session data to persistent memory this.persistentMemory.adaptationHistory.push({ sessionId: this.sessionId, duration: Date.now() - this.sessionStartTime, interactions: this.sessionMemory.interactions.length, feedback: this.sessionMemory.feedback.length, patterns: this.sessionMemory.patterns.size, timestamp: Date.now() }); // Save to storage await this.savePersistentMemory(); // Clear session memory this.sessionMemory = { interactions: [], patterns: new Map(), feedback: [], adaptations: [], contextHistory: [] }; } } export default PersonaLearningEngine;