Reason / rag.js
riosst's picture
Upload 5 files
ae8edfc verified
Raw
History Blame Contribute Delete
8.28 kB
/**
* rag.js — Wisdom Database v2.0
*
* PERUBAHAN DARI v1.0:
* - Schema wisdom diperluas: domain, confidence, source tracking
* - topicExists() untuk validasi session dari DB
* - updateTopicTitle() untuk update title setelah pesan pertama
* - Deduplication di layer DB (unique constraint)
* - getWisdomByDomain() untuk inject yang relevan saja
* - Statistik: hitungWisdom(), getDomainStats()
*/
'use strict';
const Database = require('better-sqlite3');
const path = require('path');
const crypto = require('crypto');
const dbPath = path.join(__dirname, 'reasoning_memory.db');
const db = new Database(dbPath);
// WAL mode untuk concurrent reads lebih baik
db.pragma('journal_mode = WAL');
db.pragma('synchronous = NORMAL');
// ================================================================
// SCHEMA
// ================================================================
db.exec(`
CREATE TABLE IF NOT EXISTS wisdom (
id INTEGER PRIMARY KEY AUTOINCREMENT,
domain TEXT NOT NULL DEFAULT 'general',
rule TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'auto-extracted',
confidence INTEGER NOT NULL DEFAULT 70,
use_count INTEGER NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_used_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(rule)
);
-- Legacy table compat (lessons → wisdom alias)
CREATE TABLE IF NOT EXISTS lessons (
id INTEGER PRIMARY KEY AUTOINCREMENT,
topic TEXT NOT NULL,
reasoning_rule TEXT NOT NULL,
source TEXT DEFAULT 'auto',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS chat_topics (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS chat_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
topic_id TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(topic_id) REFERENCES chat_topics(id)
);
CREATE INDEX IF NOT EXISTS idx_messages_topic ON chat_messages(topic_id);
CREATE INDEX IF NOT EXISTS idx_wisdom_domain ON wisdom(domain);
CREATE INDEX IF NOT EXISTS idx_wisdom_source ON wisdom(source);
`);
// ================================================================
// PREPARED STATEMENTS
// ================================================================
const stmts = {
// Wisdom
insertWisdom: db.prepare(
'INSERT OR IGNORE INTO wisdom (domain, rule, source, confidence) VALUES (?, ?, ?, ?)'
),
getWisdom: db.prepare(
'SELECT domain, rule, source, confidence, use_count FROM wisdom ORDER BY confidence DESC, use_count DESC, created_at DESC LIMIT 60'
),
getWisdomByDomain: db.prepare(
'SELECT domain, rule, source, confidence FROM wisdom WHERE domain LIKE ? ORDER BY confidence DESC LIMIT 10'
),
getWisdomCount: db.prepare('SELECT COUNT(*) as count FROM wisdom'),
updateUseCount: db.prepare(
'UPDATE wisdom SET use_count = use_count + 1, last_used_at = CURRENT_TIMESTAMP WHERE id = ?'
),
getDomainStats: db.prepare(
'SELECT domain, COUNT(*) as count, AVG(confidence) as avg_confidence FROM wisdom GROUP BY domain ORDER BY count DESC'
),
// Legacy lessons (backward compat)
insertLesson: db.prepare(
'INSERT INTO lessons (topic, reasoning_rule, source) VALUES (?, ?, ?)'
),
// Topics
createTopic: db.prepare('INSERT INTO chat_topics (id, title) VALUES (?, ?)'),
topicExists: db.prepare('SELECT id FROM chat_topics WHERE id = ?'),
updateTitle: db.prepare('UPDATE chat_topics SET title = ? WHERE id = ?'),
getTopics: db.prepare(
'SELECT id, title, created_at FROM chat_topics ORDER BY created_at DESC LIMIT 30'
),
// Messages
insertMessage: db.prepare(
'INSERT INTO chat_messages (topic_id, role, content) VALUES (?, ?, ?)'
),
getMessages: db.prepare(
'SELECT role, content, created_at FROM chat_messages WHERE topic_id = ? ORDER BY created_at ASC'
),
countMessages: db.prepare(
'SELECT COUNT(*) as count FROM chat_messages WHERE topic_id = ?'
)
};
// ================================================================
// EXPORTS
// ================================================================
module.exports = {
// ── WISDOM LOOP ──────────────────────────────────────────────
saveReasoning(topic, rule, source = 'auto') {
// Simpan ke tabel wisdom baru (dengan dedup)
const result = stmts.insertWisdom.run(
topic.toLowerCase().replace(/\s+/g, '_').substring(0, 50),
rule.trim().substring(0, 400),
source,
source === 'manual' ? 90 : 70
);
// Backward compat: juga simpan ke lessons
if (result.changes > 0) {
try {
stmts.insertLesson.run(topic, rule, source);
} catch (_) { /* tidak fatal */ }
console.log(`[RAG] Wisdom (${source}): ${topic}${rule.substring(0, 60)}`);
}
return result.changes > 0;
},
getAllWisdom() {
try { return stmts.getWisdom.all(); } catch { return []; }
},
getWisdomByDomain(domain) {
try { return stmts.getWisdomByDomain.all('%' + domain + '%'); } catch { return []; }
},
hitungWisdom() {
try { return stmts.getWisdomCount.get().count; } catch { return 0; }
},
getDomainStats() {
try { return stmts.getDomainStats.all(); } catch { return []; }
},
// Inject ke system prompt — prioritas wisdom dengan confidence tinggi
// dan yang relevan dengan domain (deteksi dari kata kunci)
injectMemory(contextHint = '') {
try {
let rows;
// Jika ada hint domain, inject yang relevan + general
if (contextHint) {
const domain = contextHint.toLowerCase().replace(/[^a-z0-9_/]/g, '_').substring(0, 30);
const domainRows = stmts.getWisdomByDomain.all('%' + domain + '%');
const generalRows = stmts.getWisdom.all();
// Gabung, deduplicate by rule text, ambil 50 teratas
const seen = new Set();
rows = [...domainRows, ...generalRows].filter(r => {
if (seen.has(r.rule)) return false;
seen.add(r.rule);
return true;
}).slice(0, 50);
} else {
rows = stmts.getWisdom.all();
}
if (rows.length === 0) return '';
let ctx = '\n\n[INGATAN TEKNIS — RULES YANG SUDAH TERBUKTI]:\n';
ctx += '(Gunakan jika relevan. Jangan disebut jika tidak relevan.)\n';
rows.forEach((row, i) => {
const conf = row.confidence >= 90 ? '★' : row.confidence >= 80 ? '◆' : '·';
ctx += `${i + 1}. ${conf} [${row.domain}] ${row.rule}\n`;
});
return ctx;
} catch { return ''; }
},
// ── TOPICS ───────────────────────────────────────────────────
createTopic(title) {
const id = crypto.randomBytes(8).toString('hex');
try {
stmts.createTopic.run(id, (title || 'Sesi').substring(0, 120));
} catch (e) {
console.error('[DB] createTopic error:', e.message);
}
return id;
},
topicExists(id) {
try {
const row = stmts.topicExists.get(id);
return !!row;
} catch { return false; }
},
updateTopicTitle(id, title) {
try {
stmts.updateTitle.run((title || 'Sesi').substring(0, 120), id);
} catch { /* tidak fatal */ }
},
getTopics() {
try { return stmts.getTopics.all(); } catch { return []; }
},
// ── MESSAGES ─────────────────────────────────────────────────
saveMessage(topicId, role, content) {
try {
stmts.insertMessage.run(topicId, role, (content || '').substring(0, 32000));
} catch (e) {
console.error('[DB] saveMessage error:', e.message);
}
},
getChatHistory(topicId) {
try { return stmts.getMessages.all(topicId); } catch { return []; }
},
countMessages(topicId) {
try { return stmts.countMessages.get(topicId).count; } catch { return 0; }
}
};