579 lines
16 KiB
JavaScript
579 lines
16 KiB
JavaScript
/**
|
|
* Shadow Learner - Pattern learning and prediction system
|
|
* Node.js implementation
|
|
*/
|
|
|
|
const fs = require('fs-extra');
|
|
const path = require('path');
|
|
|
|
class ConfidenceCalculator {
|
|
/**
|
|
* Calculate confidence for command failure patterns
|
|
*/
|
|
static calculateCommandConfidence(successCount, failureCount, recencyFactor) {
|
|
const totalAttempts = successCount + failureCount;
|
|
if (totalAttempts === 0) return 0.0;
|
|
|
|
// Base confidence from failure rate
|
|
const failureRate = failureCount / totalAttempts;
|
|
|
|
// Sample size adjustment (more data = more confidence)
|
|
const sampleFactor = Math.min(1.0, totalAttempts / 10.0); // Plateau at 10 samples
|
|
|
|
// Time decay (recent failures are more relevant)
|
|
const confidence = failureRate * sampleFactor * (0.5 + 0.5 * recencyFactor);
|
|
|
|
return Math.min(0.99, Math.max(0.1, confidence)); // Clamp between 0.1 and 0.99
|
|
}
|
|
|
|
/**
|
|
* Calculate confidence for tool sequence patterns
|
|
*/
|
|
static calculateSequenceConfidence(successfulSequences, totalSequences) {
|
|
if (totalSequences === 0) return 0.0;
|
|
|
|
const successRate = successfulSequences / totalSequences;
|
|
const sampleFactor = Math.min(1.0, totalSequences / 5.0);
|
|
|
|
return successRate * sampleFactor;
|
|
}
|
|
}
|
|
|
|
class PatternMatcher {
|
|
constructor(db) {
|
|
this.db = db;
|
|
}
|
|
|
|
/**
|
|
* Find similar command patterns using fuzzy matching
|
|
*/
|
|
fuzzyCommandMatch(command, threshold = 0.8) {
|
|
const cmdTokens = command.toLowerCase().split(' ');
|
|
if (cmdTokens.length === 0) return [];
|
|
|
|
const baseCmd = cmdTokens[0];
|
|
const matches = [];
|
|
|
|
for (const pattern of Object.values(this.db.commandPatterns)) {
|
|
const patternCmd = (pattern.trigger.command || '').toLowerCase();
|
|
|
|
// Exact match
|
|
if (patternCmd === baseCmd) {
|
|
matches.push(pattern);
|
|
}
|
|
// Fuzzy match on command name
|
|
else if (this._similarity(patternCmd, baseCmd) > threshold) {
|
|
matches.push(pattern);
|
|
}
|
|
// Partial match (e.g., "pip3" matches "pip install")
|
|
else if (cmdTokens.some(token => patternCmd.includes(token))) {
|
|
matches.push(pattern);
|
|
}
|
|
}
|
|
|
|
return matches.sort((a, b) => b.confidence - a.confidence);
|
|
}
|
|
|
|
/**
|
|
* Match patterns based on current context
|
|
*/
|
|
contextPatternMatch(currentContext) {
|
|
const matches = [];
|
|
|
|
for (const pattern of Object.values(this.db.contextPatterns)) {
|
|
if (this._contextMatches(currentContext, pattern.trigger)) {
|
|
matches.push(pattern);
|
|
}
|
|
}
|
|
|
|
return matches.sort((a, b) => b.confidence - a.confidence);
|
|
}
|
|
|
|
/**
|
|
* Simple string similarity (Jaccard similarity)
|
|
*/
|
|
_similarity(str1, str2) {
|
|
const set1 = new Set(str1.split(''));
|
|
const set2 = new Set(str2.split(''));
|
|
|
|
const intersection = new Set([...set1].filter(x => set2.has(x)));
|
|
const union = new Set([...set1, ...set2]);
|
|
|
|
return intersection.size / union.size;
|
|
}
|
|
|
|
/**
|
|
* Check if current context matches trigger conditions
|
|
*/
|
|
_contextMatches(current, trigger) {
|
|
for (const [key, expectedValue] of Object.entries(trigger)) {
|
|
if (!(key in current)) return false;
|
|
|
|
const currentValue = current[key];
|
|
|
|
// Handle different value types
|
|
if (typeof expectedValue === 'string' && typeof currentValue === 'string') {
|
|
if (!currentValue.toLowerCase().includes(expectedValue.toLowerCase())) {
|
|
return false;
|
|
}
|
|
} else if (expectedValue !== currentValue) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
class LearningEngine {
|
|
constructor(db) {
|
|
this.db = db;
|
|
this.confidenceCalc = ConfidenceCalculator;
|
|
}
|
|
|
|
/**
|
|
* Main learning entry point
|
|
*/
|
|
learnFromExecution(execution) {
|
|
// Learn command patterns
|
|
if (execution.tool === 'Bash') {
|
|
this._learnCommandPattern(execution);
|
|
}
|
|
|
|
// Learn tool sequences
|
|
this._learnSequencePattern(execution);
|
|
|
|
// Learn context patterns
|
|
if (!execution.success) {
|
|
this._learnFailureContext(execution);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Learn from bash command executions
|
|
*/
|
|
_learnCommandPattern(execution) {
|
|
const command = execution.parameters.command || '';
|
|
if (!command) return;
|
|
|
|
const baseCmd = command.split(' ')[0];
|
|
const patternId = `cmd_${baseCmd}`;
|
|
|
|
if (patternId in this.db.commandPatterns) {
|
|
const pattern = this.db.commandPatterns[patternId];
|
|
|
|
// Update statistics
|
|
if (execution.success) {
|
|
pattern.prediction.successCount = (pattern.prediction.successCount || 0) + 1;
|
|
} else {
|
|
pattern.prediction.failureCount = (pattern.prediction.failureCount || 0) + 1;
|
|
}
|
|
|
|
// Recalculate confidence
|
|
const recency = this._calculateRecency(execution.timestamp);
|
|
pattern.confidence = this.confidenceCalc.calculateCommandConfidence(
|
|
pattern.prediction.successCount || 0,
|
|
pattern.prediction.failureCount || 0,
|
|
recency
|
|
);
|
|
pattern.lastSeen = execution.timestamp;
|
|
pattern.evidenceCount += 1;
|
|
|
|
} else {
|
|
// Create new pattern
|
|
this.db.commandPatterns[patternId] = {
|
|
patternId,
|
|
patternType: 'command_execution',
|
|
trigger: { command: baseCmd },
|
|
prediction: {
|
|
successCount: execution.success ? 1 : 0,
|
|
failureCount: execution.success ? 0 : 1,
|
|
commonErrors: execution.errorMessage ? [execution.errorMessage] : []
|
|
},
|
|
confidence: 0.3, // Start with low confidence
|
|
evidenceCount: 1,
|
|
lastSeen: execution.timestamp,
|
|
successRate: execution.success ? 1.0 : 0.0
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Learn from tool sequence patterns
|
|
*/
|
|
_learnSequencePattern(execution) {
|
|
// Get recent tool history (last 5 tools)
|
|
const recentTools = this.db.executionHistory.slice(-5).map(e => e.tool);
|
|
recentTools.push(execution.tool);
|
|
|
|
// Look for sequences of 2-3 tools
|
|
for (let seqLen = 2; seqLen <= 3; seqLen++) {
|
|
if (recentTools.length >= seqLen) {
|
|
const sequence = recentTools.slice(-seqLen);
|
|
const patternId = `seq_${sequence.join('_')}`;
|
|
|
|
// Update or create sequence pattern
|
|
// (Simplified implementation - could be expanded)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Learn from failure contexts
|
|
*/
|
|
_learnFailureContext(execution) {
|
|
if (!execution.errorMessage) return;
|
|
|
|
// Extract key error indicators
|
|
const errorKey = this._extractErrorKey(execution.errorMessage);
|
|
if (!errorKey) return;
|
|
|
|
const patternId = `ctx_error_${errorKey}`;
|
|
|
|
if (patternId in this.db.contextPatterns) {
|
|
const pattern = this.db.contextPatterns[patternId];
|
|
pattern.evidenceCount += 1;
|
|
pattern.lastSeen = execution.timestamp;
|
|
// Update confidence based on repeated failures
|
|
pattern.confidence = Math.min(0.95, pattern.confidence + 0.05);
|
|
} else {
|
|
// Create new context pattern
|
|
this.db.contextPatterns[patternId] = {
|
|
patternId,
|
|
patternType: 'context_error',
|
|
trigger: {
|
|
tool: execution.tool,
|
|
errorType: errorKey
|
|
},
|
|
prediction: {
|
|
likelyError: execution.errorMessage,
|
|
suggestions: this._generateSuggestions(execution)
|
|
},
|
|
confidence: 0.4,
|
|
evidenceCount: 1,
|
|
lastSeen: execution.timestamp,
|
|
successRate: 0.0
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate recency factor (1.0 = very recent, 0.0 = very old)
|
|
*/
|
|
_calculateRecency(timestamp) {
|
|
const now = new Date();
|
|
const ageHours = (now - new Date(timestamp)) / (1000 * 60 * 60);
|
|
|
|
// Exponential decay: recent events matter more
|
|
return Math.max(0.0, Math.exp(-ageHours / 24.0)); // 24 hour half-life
|
|
}
|
|
|
|
/**
|
|
* Extract key error indicators from error messages
|
|
*/
|
|
_extractErrorKey(errorMessage) {
|
|
const message = errorMessage.toLowerCase();
|
|
|
|
const errorPatterns = {
|
|
'command_not_found': ['command not found', 'not found'],
|
|
'permission_denied': ['permission denied', 'access denied'],
|
|
'file_not_found': ['no such file', 'file not found'],
|
|
'connection_error': ['connection refused', 'network unreachable'],
|
|
'syntax_error': ['syntax error', 'invalid syntax']
|
|
};
|
|
|
|
for (const [errorType, patterns] of Object.entries(errorPatterns)) {
|
|
if (patterns.some(pattern => message.includes(pattern))) {
|
|
return errorType;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Generate suggestions based on failed execution
|
|
*/
|
|
_generateSuggestions(execution) {
|
|
const suggestions = [];
|
|
|
|
if (execution.tool === 'Bash') {
|
|
const command = execution.parameters.command || '';
|
|
if (command) {
|
|
const baseCmd = command.split(' ')[0];
|
|
|
|
// Common command alternatives
|
|
const alternatives = {
|
|
'pip': ['pip3', 'python -m pip', 'python3 -m pip'],
|
|
'python': ['python3'],
|
|
'node': ['nodejs'],
|
|
'vim': ['nvim', 'nano']
|
|
};
|
|
|
|
if (baseCmd in alternatives) {
|
|
const remainingArgs = command.split(' ').slice(1).join(' ');
|
|
suggestions.push(
|
|
...alternatives[baseCmd].map(alt => `Try '${alt} ${remainingArgs}'`)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return suggestions;
|
|
}
|
|
}
|
|
|
|
class PredictionEngine {
|
|
constructor(matcher) {
|
|
this.matcher = matcher;
|
|
}
|
|
|
|
/**
|
|
* Predict if a command will succeed and suggest alternatives
|
|
*/
|
|
predictCommandOutcome(command, context = {}) {
|
|
// Find matching patterns
|
|
const commandPatterns = this.matcher.fuzzyCommandMatch(command);
|
|
const contextPatterns = this.matcher.contextPatternMatch(context);
|
|
|
|
const prediction = {
|
|
likelySuccess: true,
|
|
confidence: 0.5,
|
|
warnings: [],
|
|
suggestions: []
|
|
};
|
|
|
|
// Analyze command patterns
|
|
for (const pattern of commandPatterns.slice(0, 3)) { // Top 3 matches
|
|
if (pattern.confidence > 0.7) {
|
|
const failureRate = (pattern.prediction.failureCount || 0) / Math.max(1, pattern.evidenceCount);
|
|
|
|
if (failureRate > 0.6) { // High failure rate
|
|
prediction.likelySuccess = false;
|
|
prediction.confidence = pattern.confidence;
|
|
prediction.warnings.push(`Command '${command.split(' ')[0]}' often fails`);
|
|
|
|
// Add suggestions from pattern
|
|
const suggestions = pattern.prediction.suggestions || [];
|
|
prediction.suggestions.push(...suggestions);
|
|
}
|
|
}
|
|
}
|
|
|
|
return prediction;
|
|
}
|
|
}
|
|
|
|
class ShadowLearner {
|
|
constructor(storagePath = '.claude_hooks/patterns') {
|
|
this.storagePath = path.resolve(storagePath);
|
|
fs.ensureDirSync(this.storagePath);
|
|
|
|
this.db = this._loadDatabase();
|
|
this.matcher = new PatternMatcher(this.db);
|
|
this.learningEngine = new LearningEngine(this.db);
|
|
this.predictionEngine = new PredictionEngine(this.matcher);
|
|
|
|
// Performance cache (simple in-memory cache)
|
|
this.predictionCache = new Map();
|
|
this.cacheTimeout = 5 * 60 * 1000; // 5 minutes
|
|
}
|
|
|
|
/**
|
|
* Learn from tool execution
|
|
*/
|
|
learnFromExecution(execution) {
|
|
try {
|
|
this.learningEngine.learnFromExecution(execution);
|
|
this.db.executionHistory.push(execution);
|
|
|
|
// Trim history to keep memory usage reasonable
|
|
if (this.db.executionHistory.length > 1000) {
|
|
this.db.executionHistory = this.db.executionHistory.slice(-500);
|
|
}
|
|
|
|
} catch (error) {
|
|
// Learning failures shouldn't break the system
|
|
console.error('Shadow learner error:', error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Predict command outcome with caching
|
|
*/
|
|
predictCommandOutcome(command, context = {}) {
|
|
const cacheKey = `cmd_pred:${this._hash(command)}`;
|
|
const now = Date.now();
|
|
|
|
// Check cache
|
|
if (this.predictionCache.has(cacheKey)) {
|
|
const cached = this.predictionCache.get(cacheKey);
|
|
if (now - cached.timestamp < this.cacheTimeout) {
|
|
return cached.prediction;
|
|
}
|
|
}
|
|
|
|
const prediction = this.predictionEngine.predictCommandOutcome(command, context);
|
|
|
|
// Cache result
|
|
this.predictionCache.set(cacheKey, { prediction, timestamp: now });
|
|
|
|
// Clean old cache entries
|
|
this._cleanCache();
|
|
|
|
return prediction;
|
|
}
|
|
|
|
/**
|
|
* Quick method for command failure learning (backward compatibility)
|
|
*/
|
|
learnCommandFailure(command, suggestion, confidence) {
|
|
const execution = {
|
|
tool: 'Bash',
|
|
parameters: { command },
|
|
success: false,
|
|
timestamp: new Date(),
|
|
errorMessage: `Command failed: ${command}`
|
|
};
|
|
|
|
this.learnFromExecution(execution);
|
|
|
|
// Also store the specific suggestion
|
|
const baseCmd = command.split(' ')[0];
|
|
const patternId = `cmd_${baseCmd}`;
|
|
|
|
if (patternId in this.db.commandPatterns) {
|
|
const pattern = this.db.commandPatterns[patternId];
|
|
pattern.prediction.suggestions = pattern.prediction.suggestions || [];
|
|
if (!pattern.prediction.suggestions.includes(suggestion)) {
|
|
pattern.prediction.suggestions.push(suggestion);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get suggestion for a command (backward compatibility)
|
|
*/
|
|
getSuggestion(command) {
|
|
const prediction = this.predictCommandOutcome(command);
|
|
|
|
if (!prediction.likelySuccess && prediction.suggestions.length > 0) {
|
|
return {
|
|
suggestion: prediction.suggestions[0].replace(/^Try '|'$/g, ''), // Clean format
|
|
confidence: prediction.confidence
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Save learned patterns to disk
|
|
*/
|
|
async saveDatabase() {
|
|
try {
|
|
const patternsFile = path.join(this.storagePath, 'patterns.json');
|
|
const backupFile = path.join(this.storagePath, 'patterns.backup.json');
|
|
|
|
// Create backup of existing data
|
|
if (await fs.pathExists(patternsFile)) {
|
|
await fs.move(patternsFile, backupFile, { overwrite: true });
|
|
}
|
|
|
|
// Save new data
|
|
await fs.writeJson(patternsFile, this._serializeDatabase(), { spaces: 2 });
|
|
|
|
} catch (error) {
|
|
// Save failures shouldn't break the system
|
|
console.error('Failed to save shadow learner database:', error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load patterns database from disk
|
|
*/
|
|
_loadDatabase() {
|
|
const patternsFile = path.join(this.storagePath, 'patterns.json');
|
|
|
|
try {
|
|
if (fs.existsSync(patternsFile)) {
|
|
const data = fs.readJsonSync(patternsFile);
|
|
return this._deserializeDatabase(data);
|
|
}
|
|
} catch (error) {
|
|
// If loading fails, start with empty database
|
|
console.error('Failed to load shadow learner database, starting fresh:', error.message);
|
|
}
|
|
|
|
return {
|
|
commandPatterns: {},
|
|
contextPatterns: {},
|
|
sequencePatterns: {},
|
|
executionHistory: []
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Serialize database for JSON storage
|
|
*/
|
|
_serializeDatabase() {
|
|
return {
|
|
commandPatterns: this.db.commandPatterns,
|
|
contextPatterns: this.db.contextPatterns,
|
|
sequencePatterns: this.db.sequencePatterns || {},
|
|
executionHistory: this.db.executionHistory.slice(-100), // Keep last 100 executions
|
|
metadata: {
|
|
version: '1.0.0',
|
|
lastSaved: new Date().toISOString()
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Deserialize database from JSON
|
|
*/
|
|
_deserializeDatabase(data) {
|
|
return {
|
|
commandPatterns: data.commandPatterns || {},
|
|
contextPatterns: data.contextPatterns || {},
|
|
sequencePatterns: data.sequencePatterns || {},
|
|
executionHistory: (data.executionHistory || []).map(e => ({
|
|
...e,
|
|
timestamp: new Date(e.timestamp)
|
|
}))
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Simple hash function for cache keys
|
|
*/
|
|
_hash(str) {
|
|
let hash = 0;
|
|
for (let i = 0; i < str.length; i++) {
|
|
const char = str.charCodeAt(i);
|
|
hash = ((hash << 5) - hash) + char;
|
|
hash = hash & hash; // Convert to 32bit integer
|
|
}
|
|
return hash.toString();
|
|
}
|
|
|
|
/**
|
|
* Clean expired cache entries
|
|
*/
|
|
_cleanCache() {
|
|
const now = Date.now();
|
|
for (const [key, value] of this.predictionCache.entries()) {
|
|
if (now - value.timestamp > this.cacheTimeout) {
|
|
this.predictionCache.delete(key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
ShadowLearner,
|
|
ConfidenceCalculator,
|
|
PatternMatcher,
|
|
LearningEngine,
|
|
PredictionEngine
|
|
}; |