UNPKG

aiwf

Version:

AI Workflow Framework for Claude Code with multi-language support (Korean/English)

711 lines (604 loc) 20.5 kB
/** * AI Persona Manager * Manages AI behavior personas for different development tasks */ import fs from 'fs/promises'; import path from 'path'; import { EventEmitter } from 'events'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Import related modules import { ContextEngine } from './context-engine.js'; import { MetricsCollector } from './metrics-collector.js'; import { TaskAnalyzer } from './task-analyzer.js'; class AIPersonaManager extends EventEmitter { constructor(options = {}) { super(); this.options = { personaConfigPath: options.personaConfigPath || path.join(process.cwd(), '.aiwf', 'personas'), autoDetectionEnabled: options.autoDetectionEnabled !== false, metricsEnabled: options.metricsEnabled !== false, cacheTimeout: options.cacheTimeout || 300000, // 5 minutes ...options }; this.currentPersona = null; this.personaHistory = []; this.availablePersonas = {}; this.lastSwitchTime = null; // Initialize sub-systems this.contextEngine = new ContextEngine(this.options); this.metricsCollector = new MetricsCollector(this.options); this.taskAnalyzer = new TaskAnalyzer(this.options); // Cache for persona configurations this.personaCache = new Map(); } /** * Initialize the persona system */ async init() { try { // Ensure persona directory exists await fs.mkdir(this.options.personaConfigPath, { recursive: true }); // Load available personas await this.loadAvailablePersonas(); // Initialize sub-systems await this.contextEngine.init(); if (this.options.metricsEnabled) { await this.metricsCollector.init(); } // Set default persona if (!this.currentPersona) { await this.switchPersona('developer', { silent: true }); } this.emit('initialized', { availablePersonas: Object.keys(this.availablePersonas), currentPersona: this.currentPersona?.name }); return true; } catch (error) { this.emit('error', { error, phase: 'initialization' }); throw error; } } /** * Load available persona configurations */ async loadAvailablePersonas() { // Default personas const defaultPersonas = { architect: { name: 'architect', description: 'System design and architecture', behaviors: [ 'Focus on big picture and overall system structure', 'Prioritize scalability and maintainability', 'Apply design patterns and architectural principles', 'Consider integration points and interfaces' ], focusAreas: ['system design', 'architecture', 'patterns', 'scalability'], communicationStyle: 'strategic and high-level', recommendedTools: ['Glob', 'LS', 'MarkdownProcessor', 'Read'], contextRules: { priority: ['*.md', '*.yml', '*.json', 'architecture/**'], exclude: ['test/**', 'node_modules/**', '*.test.*'], focus: ['interfaces', 'contracts', 'dependencies'], keywords: ['design', 'pattern', 'architecture', 'structure'] }, tokenAllocation: { context: 0.3, response: 0.7 } }, debugger: { name: 'debugger', description: 'Bug detection and problem solving', behaviors: [ 'Systematic and methodical approach', 'Focus on root cause analysis', 'Consider edge cases and error scenarios', 'Trace execution flow step by step' ], focusAreas: ['error handling', 'debugging', 'testing', 'validation'], communicationStyle: 'detailed and analytical', recommendedTools: ['Grep', 'Read', 'Bash', 'MemoryProfiler'], contextRules: { priority: ['*.log', '*.test.*', 'error.**', 'debug/**'], exclude: ['docs/**', '*.md'], focus: ['errors', 'exceptions', 'stack traces', 'logs'], keywords: ['error', 'bug', 'fix', 'debug', 'trace'] }, tokenAllocation: { context: 0.5, response: 0.5 } }, reviewer: { name: 'reviewer', description: 'Code quality and standards', behaviors: [ 'Verify coding standards compliance', 'Identify security vulnerabilities', 'Suggest optimizations and improvements', 'Ensure best practices are followed' ], focusAreas: ['code quality', 'security', 'standards', 'best practices'], communicationStyle: 'constructive and thorough', recommendedTools: ['Grep', 'Read', 'Bash', 'FileUtils'], contextRules: { priority: ['*.js', '*.ts', '*.py', 'src/**'], exclude: ['dist/**', 'build/**', 'coverage/**'], focus: ['functions', 'classes', 'imports', 'exports'], keywords: ['review', 'quality', 'security', 'standard'] }, tokenAllocation: { context: 0.4, response: 0.6 } }, documenter: { name: 'documenter', description: 'Documentation and guides', behaviors: [ 'Write clear and understandable explanations', 'Provide practical examples', 'Ensure comprehensive coverage', 'Maintain consistent documentation style' ], focusAreas: ['documentation', 'guides', 'examples', 'tutorials'], communicationStyle: 'clear and educational', recommendedTools: ['MarkdownProcessor', 'Read', 'Write', 'TemplateEngine'], contextRules: { priority: ['*.md', 'README*', 'docs/**', 'examples/**'], exclude: ['test/**', 'node_modules/**'], focus: ['APIs', 'interfaces', 'usage', 'examples'], keywords: ['document', 'explain', 'guide', 'example'] }, tokenAllocation: { context: 0.2, response: 0.8 } }, optimizer: { name: 'optimizer', description: 'Performance optimization', behaviors: [ 'Analyze performance bottlenecks', 'Focus on efficiency and resource usage', 'Measure and benchmark improvements', 'Apply optimization techniques' ], focusAreas: ['performance', 'efficiency', 'optimization', 'benchmarking'], communicationStyle: 'data-driven and precise', recommendedTools: ['PerformanceBenchmark', 'MemoryProfiler', 'TokenTracker'], contextRules: { priority: ['*.bench.*', 'benchmark/**', 'perf/**'], exclude: ['docs/**', '*.md'], focus: ['algorithms', 'loops', 'memory', 'complexity'], keywords: ['optimize', 'performance', 'speed', 'efficiency'] }, tokenAllocation: { context: 0.6, response: 0.4 } }, developer: { name: 'developer', description: 'General development (default)', behaviors: [ 'Balanced approach to coding', 'Focus on functionality and correctness', 'Write clean and maintainable code', 'Follow project conventions' ], focusAreas: ['implementation', 'features', 'functionality', 'coding'], communicationStyle: 'practical and straightforward', recommendedTools: ['Read', 'Write', 'Edit', 'Bash'], contextRules: { priority: ['src/**', 'lib/**', '*.js', '*.ts'], exclude: ['node_modules/**', 'dist/**'], focus: ['implementation', 'logic', 'features'], keywords: ['implement', 'create', 'build', 'develop'] }, tokenAllocation: { context: 0.4, response: 0.6 } } }; // Store default personas this.availablePersonas = { ...defaultPersonas }; // Load custom personas if they exist try { const customPath = path.join(this.options.personaConfigPath, 'custom'); const files = await fs.readdir(customPath).catch(() => []); for (const file of files) { if (file.endsWith('.json')) { const content = await fs.readFile( path.join(customPath, file), 'utf-8' ); const persona = JSON.parse(content); this.availablePersonas[persona.name] = persona; } } } catch (error) { // Custom personas are optional } } /** * Switch to a different persona */ async switchPersona(personaName, options = {}) { try { // Validate persona if (!this.isValidPersona(personaName)) { throw new Error(`Invalid persona: ${personaName}`); } // Check if already in this persona if (this.currentPersona?.name === personaName && !options.force) { return this.currentPersona; } // Save current state to history if (this.currentPersona && !options.silent) { const metrics = this.options.metricsEnabled ? await this.metricsCollector.collectCurrentMetrics() : null; this.personaHistory.push({ persona: this.currentPersona, startTime: this.lastSwitchTime, endTime: new Date(), metrics }); } // Load new persona const persona = await this.loadPersona(personaName); // Apply context rules await this.contextEngine.applyPersonaRules(persona); // Update state this.currentPersona = persona; this.lastSwitchTime = new Date(); // Track switch event if (this.options.metricsEnabled && !options.silent) { this.metricsCollector.trackEvent('persona_switch', { from: this.personaHistory[this.personaHistory.length - 1]?.persona?.name, to: personaName, trigger: options.manual ? 'manual' : 'auto', reason: options.reason }); } // Emit event this.emit('personaSwitched', { persona: personaName, trigger: options.manual ? 'manual' : 'auto' }); return persona; } catch (error) { this.emit('error', { error, phase: 'persona_switch' }); throw error; } } /** * Load a specific persona configuration */ async loadPersona(personaName) { // Check cache first if (this.personaCache.has(personaName)) { const cached = this.personaCache.get(personaName); if (Date.now() - cached.timestamp < this.options.cacheTimeout) { return cached.persona; } } // Load from configuration const persona = this.availablePersonas[personaName]; if (!persona) { throw new Error(`Persona not found: ${personaName}`); } // Process and validate persona const processed = { ...persona, loadedAt: new Date(), isActive: true }; // Cache the persona this.personaCache.set(personaName, { persona: processed, timestamp: Date.now() }); return processed; } /** * Detect optimal persona for a task */ async detectOptimalPersona(taskContext) { if (!this.options.autoDetectionEnabled) { return this.currentPersona?.name || 'developer'; } try { // Analyze task const analysis = await this.taskAnalyzer.analyzeTask(taskContext); // Score each persona const scores = {}; for (const personaName of Object.keys(this.availablePersonas)) { scores[personaName] = await this.scorePersonaFit(personaName, analysis); } // Get best match const sorted = Object.entries(scores) .sort(([,a], [,b]) => b - a); const bestMatch = sorted[0][0]; const confidence = sorted[0][1]; // Only switch if confidence is high enough if (confidence > 0.7 && bestMatch !== this.currentPersona?.name) { await this.switchPersona(bestMatch, { manual: false, reason: 'auto_detection', confidence }); } return bestMatch; } catch (error) { this.emit('error', { error, phase: 'persona_detection' }); return this.currentPersona?.name || 'developer'; } } /** * Score how well a persona fits a task */ async scorePersonaFit(personaName, taskAnalysis) { const persona = this.availablePersonas[personaName]; if (!persona) return 0; let score = 0; const weights = { keywords: 0.4, patterns: 0.3, context: 0.2, history: 0.1 }; // Keyword matching const keywordScore = this.calculateKeywordScore( persona.contextRules.keywords, taskAnalysis.keywords ); score += keywordScore * weights.keywords; // Pattern matching const patternScore = taskAnalysis.scores[personaName] || 0; score += (patternScore / 100) * weights.patterns; // Context matching const contextScore = this.calculateContextScore(persona, taskAnalysis); score += contextScore * weights.context; // Historical performance const historyScore = await this.calculateHistoryScore(personaName, taskAnalysis); score += historyScore * weights.history; return Math.min(score, 1); } /** * Calculate keyword matching score */ calculateKeywordScore(personaKeywords, taskKeywords) { if (!personaKeywords?.length || !taskKeywords?.length) return 0; const matches = taskKeywords.filter(keyword => personaKeywords.some(pk => keyword.toLowerCase().includes(pk.toLowerCase()) ) ); return matches.length / taskKeywords.length; } /** * Calculate context matching score */ calculateContextScore(persona, analysis) { let score = 0; let factors = 0; // File type matching if (analysis.recentFiles?.length) { const priorityPatterns = persona.contextRules.priority || []; const matchingFiles = analysis.recentFiles.filter(file => priorityPatterns.some(pattern => this.matchesPattern(file, pattern) ) ); score += matchingFiles.length / analysis.recentFiles.length; factors++; } // Error state bonus for debugger if (persona.name === 'debugger' && analysis.errorState?.hasErrors) { score += 1; factors++; } // Git context bonus if (persona.name === 'reviewer' && analysis.gitContext?.isPullRequest) { score += 1; factors++; } return factors > 0 ? score / factors : 0; } /** * Calculate historical performance score */ async calculateHistoryScore(personaName, analysis) { if (!this.options.metricsEnabled) return 0.5; // Neutral score const history = await this.metricsCollector.getPersonaHistory(personaName); if (!history || history.length === 0) return 0.5; // Find similar tasks in history const similarTasks = history.filter(session => this.taskAnalyzer.calculateSimilarity(session.task, analysis) > 0.6 ); if (similarTasks.length === 0) return 0.5; // Calculate average performance const avgPerformance = similarTasks.reduce((sum, task) => sum + (task.qualityMetrics?.overallScore || 0.5), 0 ) / similarTasks.length; return avgPerformance; } /** * Get current persona */ getCurrentPersona() { return this.currentPersona; } /** * Get persona context rules */ getPersonaContext(personaName) { const persona = this.availablePersonas[personaName]; return persona?.contextRules || null; } /** * Apply persona rules to content */ async applyPersonaRules(content, personaName) { const persona = personaName ? this.availablePersonas[personaName] : this.currentPersona; if (!persona) { throw new Error('No persona specified or active'); } return await this.contextEngine.filterContent(content, persona); } /** * Get available personas */ getAvailablePersonas() { return Object.keys(this.availablePersonas); } /** * Check if persona is valid */ isValidPersona(personaName) { return personaName in this.availablePersonas; } /** * Get persona statistics */ async getPersonaStats(personaName) { if (!this.options.metricsEnabled) { return { message: 'Metrics collection is disabled' }; } return await this.metricsCollector.getPersonaStats(personaName); } /** * Generate persona report */ async generateReport(options = {}) { if (!this.options.metricsEnabled) { return { message: 'Metrics collection is disabled' }; } const report = await this.metricsCollector.generateReport(options); // Add persona-specific insights report.personaInsights = {}; for (const personaName of this.getAvailablePersonas()) { report.personaInsights[personaName] = await this.generatePersonaInsights( personaName, report.personaAnalysis[personaName] ); } return report; } /** * Generate insights for a specific persona */ async generatePersonaInsights(personaName, analysis) { if (!analysis) return null; const insights = { effectiveness: this.calculateEffectiveness(analysis), recommendations: [], bestPractices: [] }; // Generate recommendations if (analysis.tokenEfficiency < 0.5) { insights.recommendations.push( 'Consider more aggressive context filtering to improve token efficiency' ); } if (analysis.averageCompletionTime > 300000) { // 5 minutes insights.recommendations.push( 'Tasks are taking longer than expected. Review task complexity or persona fit' ); } // Add best practices based on persona const persona = this.availablePersonas[personaName]; if (persona) { insights.bestPractices = this.generateBestPractices(persona, analysis); } return insights; } /** * Calculate persona effectiveness */ calculateEffectiveness(analysis) { const weights = { completionTime: 0.3, qualityScore: 0.4, tokenEfficiency: 0.3 }; let score = 0; // Normalize completion time (lower is better) const timeScore = Math.max(0, 1 - (analysis.averageCompletionTime / 600000)); score += timeScore * weights.completionTime; // Quality score (already normalized) score += (analysis.averageQualityScore || 0.5) * weights.qualityScore; // Token efficiency (already normalized) score += (analysis.tokenEfficiency || 0.5) * weights.tokenEfficiency; return { score, rating: score > 0.8 ? 'excellent' : score > 0.6 ? 'good' : score > 0.4 ? 'fair' : 'needs improvement' }; } /** * Generate best practices for a persona */ generateBestPractices(persona, analysis) { const practices = []; // General best practices practices.push(`Use ${persona.name} persona for ${persona.focusAreas.join(', ')}`); practices.push(`Recommended tools: ${persona.recommendedTools.join(', ')}`); // Specific recommendations based on analysis if (analysis.bestUseCases?.length) { practices.push(`Most effective for: ${analysis.bestUseCases.join(', ')}`); } return practices; } /** * Check if pattern matches file path */ matchesPattern(filePath, pattern) { // Convert glob pattern to regex const regex = pattern .replace(/\*\*/g, '.*') .replace(/\*/g, '[^/]*') .replace(/\?/g, '.'); return new RegExp(`^${regex}$`).test(filePath); } /** * Reset persona system */ async reset() { this.currentPersona = null; this.personaHistory = []; this.personaCache.clear(); this.lastSwitchTime = null; if (this.options.metricsEnabled) { await this.metricsCollector.reset(); } await this.contextEngine.reset(); // Set default persona await this.switchPersona('developer', { silent: true }); this.emit('reset'); } /** * Cleanup resources */ async cleanup() { this.personaCache.clear(); if (this.options.metricsEnabled) { await this.metricsCollector.cleanup(); } await this.contextEngine.cleanup(); this.removeAllListeners(); } } export default AIPersonaManager; export { AIPersonaManager };