Adds: - Critic Engine (critic.js): Validates output against correction store - Auto-detection: Identifies uncorrected patterns (quantitative without proof, vague quantification, etc.) - Auto-storage: New patterns immediately stored as corrections - NocoDB Schema: Tables for corrections, preferences, episodes, decisions, validation - Memory Service (memory-service.js): NocoDB integration layer - Response Generator (response-generator.js): End-to-end pipeline with critic - Correction Store: JSON-based with README documentation Behavior: - ALL output validated before delivery - Quantitative claims without evidence → auto-corrected - Corrections immediately block future occurrences - System learns from its own mistakes Test: node architecture/test-critic.js
330 lines
9.7 KiB
JavaScript
330 lines
9.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Self-Correcting Memory Architecture - Critic Engine
|
|
*
|
|
* Flow:
|
|
* 1. Generate response → 2. Critic checks against correction store →
|
|
* 3. If violations → Block & regenerate → 4. If new pattern → Auto-store
|
|
*
|
|
* Principles:
|
|
* - ALL output validated against correction store before delivery
|
|
* - Quantitative claims without proof → auto-correction
|
|
* - Corrections immediately block future occurrences
|
|
* - System learns from its own mistakes
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
class CriticEngine {
|
|
constructor(options = {}) {
|
|
this.correctionStorePath = options.correctionStorePath || '/home/jcbeasley/.openclaw/workspace/memory/corrections';
|
|
this.autoLearnEnabled = options.autoLearn !== false;
|
|
this.strictMode = options.strictMode || false; // Block on warnings too
|
|
|
|
// Correction patterns (loaded from store + hardcoded rules)
|
|
this.corrections = [];
|
|
this.patternRules = this.getPatternRules();
|
|
}
|
|
|
|
/**
|
|
* Pattern Rules - Auto-detect uncorrected issues
|
|
*/
|
|
getPatternRules() {
|
|
return [
|
|
{
|
|
id: 'quantitative_without_proof',
|
|
name: 'Quantitative Claim Without Evidence',
|
|
pattern: /\b\d+\s*(%|percent|times|x|percent|fold)\b/i,
|
|
exclude: /\b(according to|based on|from|measured|tested|verified|source:|citation|data:)\b/i,
|
|
severity: 'auto-correct',
|
|
message: 'Quantitative claim requires evidence',
|
|
autoStore: true
|
|
},
|
|
{
|
|
id: 'unverified_performance',
|
|
name: 'Performance Claim Without Benchmark',
|
|
pattern: /\b(faster|slower|better|worse|improved|optimized)\s+(than|by)\b/i,
|
|
exclude: /\b(measured|benchmarked|tested|profiled|verified)\b/i,
|
|
severity: 'auto-correct',
|
|
message: 'Performance claims require benchmarks',
|
|
autoStore: true
|
|
},
|
|
{
|
|
id: 'absolute_without_qualification',
|
|
name: 'Absolute Statement Without Qualification',
|
|
pattern: /\b(always|never|all|none|every|impossible)\b/i,
|
|
exclude: /\b(in this case|for this|under these|given the|based on)\b/i,
|
|
severity: 'warning',
|
|
message: 'Absolute statements need qualification',
|
|
autoStore: false // Manual review first
|
|
},
|
|
{
|
|
id: 'vague_quantification',
|
|
name: 'Vague Quantification',
|
|
pattern: /\b(many|few|several|some|most|lots|a lot)\b/i,
|
|
exclude: /\b(specifically|exactly|precisely)\b/i,
|
|
severity: 'auto-correct',
|
|
message: 'Use specific numbers or omit',
|
|
autoStore: true
|
|
},
|
|
{
|
|
id: 'unverified_completion',
|
|
name: '"Done" Without Verification',
|
|
pattern: /\b(done|complete|finished|working)\b/i,
|
|
exclude: /\b(verified|tested|confirmed|validated|checked)\b/i,
|
|
severity: 'error',
|
|
message: 'Completion claims require verification evidence',
|
|
autoStore: true
|
|
}
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Load corrections from store
|
|
*/
|
|
async loadCorrections() {
|
|
// Load from NocoDB (to be implemented) or local JSON
|
|
const correctionsFile = path.join(this.correctionStorePath, '_index.json');
|
|
try {
|
|
if (fs.existsSync(correctionsFile)) {
|
|
const data = JSON.parse(fs.readFileSync(correctionsFile, 'utf8'));
|
|
this.corrections = data.corrections || [];
|
|
}
|
|
} catch (err) {
|
|
console.warn('Could not load corrections:', err.message);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Main Critic Check
|
|
* Returns: { passed: bool, violations: [], newPatterns: [], corrected: string }
|
|
*/
|
|
async critique(response, context = {}) {
|
|
await this.loadCorrections();
|
|
|
|
const violations = [];
|
|
const newPatterns = [];
|
|
|
|
// 1. Check against known corrections (blocking)
|
|
const knownViolations = this.checkKnownCorrections(response);
|
|
violations.push(...knownViolations);
|
|
|
|
// 2. Auto-detect new patterns (if enabled)
|
|
if (this.autoLearnEnabled && violations.length === 0) {
|
|
const detectedPatterns = this.detectNewPatterns(response, context);
|
|
|
|
for (const pattern of detectedPatterns) {
|
|
if (pattern.severity === 'auto-correct') {
|
|
// Auto-store as new correction
|
|
await this.storeCorrection(pattern);
|
|
violations.push({
|
|
...pattern,
|
|
message: `${pattern.message} [AUTO-STORED AS CORRECTION]`,
|
|
autoStored: true
|
|
});
|
|
} else if (pattern.severity === 'warning') {
|
|
newPatterns.push(pattern);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Generate corrected version if violations found
|
|
let corrected = response;
|
|
if (violations.length > 0) {
|
|
corrected = await this.generateCorrection(response, violations);
|
|
}
|
|
|
|
return {
|
|
passed: violations.length === 0,
|
|
blocked: violations.length > 0 && violations.some(v => v.blocking),
|
|
violations,
|
|
newPatterns,
|
|
corrected,
|
|
original: response
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check against known corrections (from store)
|
|
*/
|
|
checkKnownCorrections(response) {
|
|
const violations = [];
|
|
|
|
for (const correction of this.corrections) {
|
|
const pattern = new RegExp(correction.pattern, 'i');
|
|
|
|
if (pattern.test(response)) {
|
|
// Check if exclusion applies
|
|
if (correction.excludePattern) {
|
|
const exclude = new RegExp(correction.excludePattern, 'i');
|
|
if (exclude.test(response)) {
|
|
continue; // Has exclusion, skip
|
|
}
|
|
}
|
|
|
|
violations.push({
|
|
id: correction.id,
|
|
type: 'known-correction',
|
|
severity: correction.severity || 'error',
|
|
blocking: correction.blocking !== false,
|
|
message: correction.message,
|
|
suggestion: correction.suggestion,
|
|
originalPattern: correction.pattern
|
|
});
|
|
}
|
|
}
|
|
|
|
return violations;
|
|
}
|
|
|
|
/**
|
|
* Detect new patterns that should become corrections
|
|
*/
|
|
detectNewPatterns(response, context) {
|
|
const detected = [];
|
|
|
|
for (const rule of this.patternRules) {
|
|
if (rule.pattern.test(response)) {
|
|
// Check if exclusion applies
|
|
if (rule.exclude && rule.exclude.test(response)) {
|
|
continue;
|
|
}
|
|
|
|
detected.push({
|
|
id: rule.id,
|
|
type: 'auto-detected',
|
|
severity: rule.severity,
|
|
message: rule.message,
|
|
pattern: rule.pattern.source,
|
|
autoStore: rule.autoStore,
|
|
context: context.task || 'unknown'
|
|
});
|
|
}
|
|
}
|
|
|
|
return detected;
|
|
}
|
|
|
|
/**
|
|
* Store new correction (immediate write)
|
|
*/
|
|
async storeCorrection(pattern) {
|
|
const correction = {
|
|
id: `correction_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
pattern: pattern.pattern,
|
|
severity: pattern.severity,
|
|
message: pattern.message,
|
|
suggestion: `Add evidence or qualification`,
|
|
blocking: true,
|
|
autoDetected: true,
|
|
createdAt: new Date().toISOString(),
|
|
context: pattern.context
|
|
};
|
|
|
|
// Add to memory
|
|
this.corrections.push(correction);
|
|
|
|
// Persist to disk (immediate)
|
|
await this.persistCorrections();
|
|
|
|
// Log for audit
|
|
console.log(`[CRITIC] Auto-stored correction: ${correction.id} - ${pattern.message}`);
|
|
|
|
return correction;
|
|
}
|
|
|
|
/**
|
|
* Persist corrections to disk (will move to NocoDB)
|
|
*/
|
|
async persistCorrections() {
|
|
try {
|
|
if (!fs.existsSync(this.correctionStorePath)) {
|
|
fs.mkdirSync(this.correctionStorePath, { recursive: true });
|
|
}
|
|
|
|
const indexFile = path.join(this.correctionStorePath, '_index.json');
|
|
fs.writeFileSync(indexFile, JSON.stringify({
|
|
version: 1,
|
|
updatedAt: new Date().toISOString(),
|
|
corrections: this.corrections
|
|
}, null, 2));
|
|
|
|
// Also write individual correction files for inspection
|
|
for (const correction of this.corrections.slice(-10)) { // Last 10
|
|
const filePath = path.join(this.correctionStorePath, `${correction.id}.json`);
|
|
fs.writeFileSync(filePath, JSON.stringify(correction, null, 2));
|
|
}
|
|
} catch (err) {
|
|
console.error('[CRITIC] Failed to persist corrections:', err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate corrected version of response
|
|
*/
|
|
async generateCorrection(original, violations) {
|
|
// Simple correction: add warning header
|
|
const warnings = violations.map(v => `- ${v.message}`).join('\n');
|
|
|
|
return `⚠️ CORRECTIONS REQUIRED:\n${warnings}\n\n---\n\n${original}`;
|
|
|
|
// Future: Use LLM to actually fix the content
|
|
// const fixPrompt = buildFixPrompt(original, violations);
|
|
// return await llm.generate(fixPrompt);
|
|
}
|
|
|
|
/**
|
|
* Manual correction entry (for user feedback)
|
|
*/
|
|
async addManualCorrection(originalText, correctionText, reason) {
|
|
const correction = {
|
|
id: `manual_${Date.now()}`,
|
|
pattern: this.escapeRegex(originalText.substring(0, 100)),
|
|
severity: 'error',
|
|
message: reason,
|
|
suggestion: correctionText,
|
|
blocking: true,
|
|
manualEntry: true,
|
|
createdAt: new Date().toISOString()
|
|
};
|
|
|
|
this.corrections.push(correction);
|
|
await this.persistCorrections();
|
|
|
|
return correction;
|
|
}
|
|
|
|
/**
|
|
* Escape regex special chars
|
|
*/
|
|
escapeRegex(str) {
|
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
}
|
|
|
|
// Export
|
|
module.exports = CriticEngine;
|
|
|
|
// CLI usage
|
|
if (require.main === module) {
|
|
const responseFile = process.argv[2];
|
|
if (!responseFile) {
|
|
console.error('Usage: node critic.js <response-file>');
|
|
process.exit(1);
|
|
}
|
|
|
|
const response = fs.readFileSync(responseFile, 'utf8');
|
|
const critic = new CriticEngine();
|
|
|
|
critic.critique(response)
|
|
.then(result => {
|
|
console.log(JSON.stringify(result, null, 2));
|
|
})
|
|
.catch(err => {
|
|
console.error('Critic error:', err);
|
|
process.exit(1);
|
|
});
|
|
}
|