| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| '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); |
|
|
| |
| db.pragma('journal_mode = WAL'); |
| db.pragma('synchronous = NORMAL'); |
|
|
| |
| |
| |
| 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); |
| `); |
|
|
| |
| |
| |
| const stmts = { |
| |
| 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' |
| ), |
|
|
| |
| insertLesson: db.prepare( |
| 'INSERT INTO lessons (topic, reasoning_rule, source) VALUES (?, ?, ?)' |
| ), |
|
|
| |
| 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' |
| ), |
|
|
| |
| 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 = ?' |
| ) |
| }; |
|
|
| |
| |
| |
| module.exports = { |
|
|
| |
|
|
| saveReasoning(topic, rule, source = 'auto') { |
| |
| const result = stmts.insertWisdom.run( |
| topic.toLowerCase().replace(/\s+/g, '_').substring(0, 50), |
| rule.trim().substring(0, 400), |
| source, |
| source === 'manual' ? 90 : 70 |
| ); |
|
|
| |
| if (result.changes > 0) { |
| try { |
| stmts.insertLesson.run(topic, rule, source); |
| } catch (_) { } |
| 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 []; } |
| }, |
|
|
| |
| |
| injectMemory(contextHint = '') { |
| try { |
| let rows; |
|
|
| |
| 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(); |
|
|
| |
| 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 ''; } |
| }, |
|
|
| |
|
|
| 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 { } |
| }, |
|
|
| getTopics() { |
| try { return stmts.getTopics.all(); } catch { return []; } |
| }, |
|
|
| |
|
|
| 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; } |
| } |
| }; |
|
|