#!/usr/bin/env node /** * Memory Service - NocoDB Integration * * Uses ai_data_Memory table (mx149yctebfwvys) in Agent base (pedwxnsn51vxdq2) * Single table with 'type' field to distinguish record types */ 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.baseId = config.baseId || 'pedwxnsn51vxdq2'; this.tableId = config.tableId || 'mx149yctebfwvys'; // ai_data_Memory } /** * Get HTTP client with auth */ getClient() { return axios.create({ baseURL: `${this.nocodbUrl}/api/v2/tables/${this.tableId}`, 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('/records', { params: { where: `(type,eq,correction)~and(blocking,eq,true)`, limit: 1000 } }); return response.data.list.map(this.parseCorrectionRecord); } /** * Store new correction */ async storeCorrection(correction) { const client = this.getClient(); const payload = { type: 'correction', 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('/records', payload); return this.parseCorrectionRecord(response.data); } /** * Increment hit count for correction */ async incrementCorrectionHit(correctionId) { const client = this.getClient(); // Get current record const current = await client.get(`/records/${correctionId}`); const newCount = (current.data.hit_count || 0) + 1; await client.patch(`/records/${correctionId}`, { hit_count: newCount, last_triggered: new Date().toISOString(), updated_at: new Date().toISOString() }); return newCount; } parseCorrectionRecord(record) { return { id: record.id, pattern: record.pattern, excludePattern: record.exclude_pattern, severity: record.severity, message: record.message, suggestion: record.suggestion, blocking: record.blocking, autoDetected: record.auto_detected, manualEntry: record.manual_entry, hitCount: record.hit_count, lastTriggered: record.last_triggered, context: record.context, createdAt: record.created_at }; } // ============================================ // PREFERENCES API // ============================================ /** * Get preferences by category */ async getPreferences(category, minImportance = 5) { const client = this.getClient(); const response = await client.get('/records', { params: { where: `(type,eq,preference)~and(category,eq,${category})~and(importance,gte,${minImportance})`, sort: '-importance,-access_count' } }); return response.data.list.map(this.parsePreferenceRecord); } /** * Store preference */ async storePreference(pref) { const client = this.getClient(); const payload = { type: 'preference', 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('/records', payload); return this.parsePreferenceRecord(response.data); } parsePreferenceRecord(record) { return { id: record.id, category: record.category, key: record.key, value: record.value, importance: record.importance, confidence: record.confidence, tags: JSON.parse(record.tags || '[]'), accessCount: record.access_count, lastAccessed: record.last_accessed, confirmedCount: record.confirmed_count, createdAt: record.created_at }; } // ============================================ // EPISODES API // ============================================ /** * Store episodic memory */ async storeEpisode(episode) { const client = this.getClient(); const payload = { type: 'episode', 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('/records', payload); return response.data; } /** * Get recent episodes */ async getEpisodes(limit = 10) { const client = this.getClient(); const response = await client.get('/records', { params: { where: `(type,eq,episode)`, sort: '-date', limit } }); return response.data.list; } // ============================================ // DECISIONS API // ============================================ /** * Store decision */ async storeDecision(decision) { const client = this.getClient(); const payload = { type: 'decision', 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('/records', payload); return response.data; } /** * Get active decisions */ async getActiveDecisions(project) { const client = this.getClient(); const params = { where: `(type,eq,decision)~and(status,eq,active)`, sort: '-date' }; if (project) { params.where += `~and(project,eq,${project})`; } const response = await client.get('/records', { params }); return response.data.list; } // ============================================ // VALIDATION LOGGING // ============================================ /** * Log validation run */ async logValidation(run) { const client = this.getClient(); const payload = { type: 'validation', 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('/records', payload); return response.data; } /** * Get validation statistics */ async getValidationStats(days = 7) { const client = this.getClient(); const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - days); const cutoffStr = cutoff.toISOString(); const response = await client.get('/records', { params: { where: `(type,eq,validation)~and(timestamp,gte,${cutoffStr})`, 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 }; } // ============================================ // UTILITY // ============================================ /** * Test connection */ async testConnection() { const client = this.getClient(); try { const response = await client.get('/records', { params: { limit: 1 } }); return { success: true, message: 'Connected to ai_data_Memory table', recordCount: response.data.pageInfo?.totalRows || 0 }; } catch (err) { return { success: false, message: err.response?.data?.error || err.message }; } } } // Export module.exports = MemoryService;