Implement self-correcting memory architecture with critic engine
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
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Memory Service - NocoDB + Critic Integration
|
||||
*
|
||||
* Provides:
|
||||
* - Structured memory storage (preferences, episodes, decisions)
|
||||
* - Correction store (self-correcting patterns)
|
||||
* - Validation logging
|
||||
*/
|
||||
|
||||
const axios = require('axios');
|
||||
|
||||
class MemoryService {
|
||||
constructor(config = {}) {
|
||||
this.nocodbUrl = config.nocodbUrl || 'http://192.168.25.5:8080';
|
||||
this.nocodbToken = config.nocodbToken || process.env.NOCODB_TOKEN;
|
||||
this.projectId = config.projectId || 'default';
|
||||
|
||||
// Table mappings (NocoDB table IDs)
|
||||
this.tables = {
|
||||
corrections: config.correctionsTable || 'corrections',
|
||||
preferences: config.preferencesTable || 'memory_preferences',
|
||||
episodes: config.episodesTable || 'memory_episodes',
|
||||
decisions: config.decisionsTable || 'memory_decisions',
|
||||
validation: config.validationTable || 'validation_runs'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HTTP client with auth
|
||||
*/
|
||||
getClient() {
|
||||
return axios.create({
|
||||
baseURL: `${this.nocodbUrl}/api/v2/tables`,
|
||||
headers: {
|
||||
'xc-token': this.nocodbToken,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// CORRECTIONS API
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Load all active corrections
|
||||
*/
|
||||
async loadCorrections() {
|
||||
const client = this.getClient();
|
||||
const response = await client.get(`/${this.tables.corrections}/records`, {
|
||||
params: {
|
||||
where: `(blocking,eq,true)`,
|
||||
limit: 1000
|
||||
}
|
||||
});
|
||||
return response.data.list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store new correction
|
||||
*/
|
||||
async storeCorrection(correction) {
|
||||
const client = this.getClient();
|
||||
|
||||
const payload = {
|
||||
id: correction.id,
|
||||
pattern: correction.pattern,
|
||||
exclude_pattern: correction.excludePattern || null,
|
||||
severity: correction.severity,
|
||||
message: correction.message,
|
||||
suggestion: correction.suggestion,
|
||||
blocking: correction.blocking !== false,
|
||||
auto_detected: correction.autoDetected || true,
|
||||
manual_entry: correction.manualEntry || false,
|
||||
hit_count: 0,
|
||||
context: correction.context || 'unknown',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
const response = await client.post(
|
||||
`/${this.tables.corrections}/records`,
|
||||
payload
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment hit count for correction
|
||||
*/
|
||||
async incrementCorrectionHit(correctionId) {
|
||||
const client = this.getClient();
|
||||
|
||||
// Get current hit count
|
||||
const current = await client.get(
|
||||
`/${this.tables.corrections}/records/${correctionId}`
|
||||
);
|
||||
|
||||
const newCount = (current.data.hit_count || 0) + 1;
|
||||
|
||||
await client.patch(
|
||||
`/${this.tables.corrections}/records/${correctionId}`,
|
||||
{
|
||||
hit_count: newCount,
|
||||
last_triggered: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
);
|
||||
|
||||
return newCount;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// PREFERENCES API
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Get preferences by category
|
||||
*/
|
||||
async getPreferences(category, minImportance = 5) {
|
||||
const client = this.getClient();
|
||||
|
||||
const response = await client.get(`/${this.tables.preferences}/records`, {
|
||||
params: {
|
||||
where: `(category,eq,${category})~and(importance,gte,${minImportance})`,
|
||||
sort: '-importance,-access_count'
|
||||
}
|
||||
});
|
||||
|
||||
return response.data.list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store preference
|
||||
*/
|
||||
async storePreference(pref) {
|
||||
const client = this.getClient();
|
||||
|
||||
const payload = {
|
||||
id: pref.id,
|
||||
category: pref.category,
|
||||
key: pref.key,
|
||||
value: pref.value,
|
||||
importance: pref.importance || 5,
|
||||
confidence: pref.confidence || 1.0,
|
||||
tags: JSON.stringify(pref.tags || []),
|
||||
confirmed_count: pref.confirmedCount || 0,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
const response = await client.post(
|
||||
`/${this.tables.preferences}/records`,
|
||||
payload
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// EPISODES API
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Store episodic memory
|
||||
*/
|
||||
async storeEpisode(episode) {
|
||||
const client = this.getClient();
|
||||
|
||||
const payload = {
|
||||
id: episode.id,
|
||||
date: episode.date || new Date().toISOString().split('T')[0],
|
||||
summary: episode.summary,
|
||||
details: episode.details,
|
||||
project: episode.project,
|
||||
outcomes: JSON.stringify(episode.outcomes || []),
|
||||
corrections_triggered: JSON.stringify(episode.correctionsTriggered || []),
|
||||
created_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
const response = await client.post(
|
||||
`/${this.tables.episodes}/records`,
|
||||
payload
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DECISIONS API
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Store decision
|
||||
*/
|
||||
async storeDecision(decision) {
|
||||
const client = this.getClient();
|
||||
|
||||
const payload = {
|
||||
id: decision.id,
|
||||
date: decision.date || new Date().toISOString().split('T')[0],
|
||||
project: decision.project,
|
||||
decision: decision.decision,
|
||||
alternatives: JSON.stringify(decision.alternatives || []),
|
||||
rationale: decision.rationale,
|
||||
status: decision.status || 'active',
|
||||
created_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
const response = await client.post(
|
||||
`/${this.tables.decisions}/records`,
|
||||
payload
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// VALIDATION LOGGING
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Log validation run
|
||||
*/
|
||||
async logValidation(run) {
|
||||
const client = this.getClient();
|
||||
|
||||
const payload = {
|
||||
id: run.id,
|
||||
session_id: run.sessionId,
|
||||
timestamp: new Date().toISOString(),
|
||||
input_length: run.inputLength,
|
||||
output_length: run.outputLength,
|
||||
violations_found: run.violationsFound || 0,
|
||||
new_patterns_detected: run.newPatternsDetected || 0,
|
||||
corrections_auto_stored: run.correctionsAutoStored || 0,
|
||||
processing_time_ms: run.processingTimeMs,
|
||||
blocked: run.blocked || false,
|
||||
workflow: run.workflow
|
||||
};
|
||||
|
||||
const response = await client.post(
|
||||
`/${this.tables.validation}/records`,
|
||||
payload
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation statistics
|
||||
*/
|
||||
async getValidationStats(days = 7) {
|
||||
const client = this.getClient();
|
||||
|
||||
const response = await client.get(`/${this.tables.validation}/records`, {
|
||||
params: {
|
||||
where: `(timestamp,gte,${days} days ago})`,
|
||||
fields: 'blocked,violations_found,corrections_auto_stored'
|
||||
}
|
||||
});
|
||||
|
||||
const runs = response.data.list;
|
||||
|
||||
return {
|
||||
totalRuns: runs.length,
|
||||
blockedCount: runs.filter(r => r.blocked).length,
|
||||
totalViolations: runs.reduce((sum, r) => sum + (r.violations_found || 0), 0),
|
||||
totalAutoCorrections: runs.reduce((sum, r) => sum + (r.corrections_auto_stored || 0), 0),
|
||||
blockRate: runs.length > 0 ? (runs.filter(r => r.blocked).length / runs.length * 100).toFixed(1) : 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export
|
||||
module.exports = MemoryService;
|
||||
Reference in New Issue
Block a user