Files
mc-bot-town/nodejs/controller/ai/memory-db.js
T
2026-05-03 11:13:06 -04:00

434 lines
11 KiB
JavaScript

'use strict';
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const fs = require('fs');
/**
* AI Memory Database
* Stores player-specific memories and bot directives
* Singleton pattern - one instance per bot
*/
class AIMemoryDB {
constructor() {
this.db = null;
this.botName = null;
}
/**
* Initialize database connection and create tables
* @param {string} dbPath - Path to sqlite database file
* @param {string} botName - Bot name for namespacing
*/
async initialize(dbPath = './storage/ai-memory.db', botName = 'default') {
if (this.db) {
this.botName = botName;
return; // Already initialized
}
const fullPath = path.resolve(dbPath);
const dir = path.dirname(fullPath);
// Ensure directory exists
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
console.log(`AI Memory: Initializing database at ${fullPath} for bot ${botName}`);
this.db = new sqlite3.Database(fullPath);
this.botName = botName;
await this.createTables();
}
/**
* Create tables if they don't exist
*/
createTables() {
return new Promise((resolve, reject) => {
this.db.serialize(() => {
// Player memories table
this.db.run(`
CREATE TABLE IF NOT EXISTS player_memories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
bot_name TEXT NOT NULL,
player_name TEXT NOT NULL,
memory_key TEXT NOT NULL,
memory_value TEXT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(bot_name, player_name, memory_key)
)
`);
// Bot directives table - persistent instructions per bot
this.db.run(`
CREATE TABLE IF NOT EXISTS bot_directives (
id INTEGER PRIMARY KEY AUTOINCREMENT,
bot_name TEXT NOT NULL,
directive_key TEXT NOT NULL,
directive_value TEXT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(bot_name, directive_key)
)
`);
// General memories table - for non-player-specific info
this.db.run(`
CREATE TABLE IF NOT EXISTS general_memories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
bot_name TEXT NOT NULL,
memory_key TEXT NOT NULL,
memory_value TEXT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(bot_name, memory_key)
)
`);
// Create indexes for faster lookups
this.db.run(`CREATE INDEX IF NOT EXISTS idx_player_memories_lookup ON player_memories(bot_name, player_name)`);
this.db.run(`CREATE INDEX IF NOT EXISTS idx_bot_directives_lookup ON bot_directives(bot_name)`);
this.db.run(`CREATE INDEX IF NOT EXISTS idx_general_memories_lookup ON general_memories(bot_name)`);
resolve();
});
});
}
// ========================================
// Player Memories (Shared across all bots)
// ========================================
/**
* Set a memory about a specific player (shared across all bots)
*/
async setPlayerMemory(playerName, key, value) {
return new Promise((resolve, reject) => {
this.db.run(`
INSERT OR REPLACE INTO player_memories (bot_name, player_name, memory_key, memory_value, updated_at)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
`, ['global', playerName, key, value], (err) => {
if (err) reject(err);
else resolve();
});
});
}
/**
* Get a specific memory about a player (shared across all bots)
*/
async getPlayerMemory(playerName, key) {
return new Promise((resolve, reject) => {
this.db.get(`
SELECT memory_value FROM player_memories
WHERE bot_name = 'global' AND player_name = ? AND memory_key = ?
`, [playerName, key], (err, row) => {
if (err) reject(err);
else resolve(row ? row.memory_value : null);
});
});
}
/**
* Get all memories about a player (shared across all bots)
*/
async getAllPlayerMemories(playerName) {
return new Promise((resolve, reject) => {
this.db.all(`
SELECT memory_key, memory_value FROM player_memories
WHERE bot_name = 'global' AND player_name = ?
`, [playerName], (err, rows) => {
if (err) reject(err);
else {
const memories = {};
for (const row of rows) {
memories[row.memory_key] = row.memory_value;
}
resolve(memories);
}
});
});
}
/**
* Delete a specific player memory (shared across all bots)
*/
async deletePlayerMemory(playerName, key) {
return new Promise((resolve, reject) => {
this.db.run(`
DELETE FROM player_memories
WHERE bot_name = 'global' AND player_name = ? AND memory_key = ?
`, [playerName, key], (err) => {
if (err) reject(err);
else resolve();
});
});
}
/**
* List all players with stored memories (shared across all bots)
*/
async getAllKnownPlayers() {
return new Promise((resolve, reject) => {
this.db.all(`
SELECT DISTINCT player_name FROM player_memories
WHERE bot_name = 'global'
`, (err, rows) => {
if (err) reject(err);
else resolve(rows.map(r => r.player_name));
});
});
}
// ========================================
// Bot Directives
// ========================================
/**
* Set a directive for this bot
*/
async setDirective(key, value) {
return new Promise((resolve, reject) => {
this.db.run(`
INSERT OR REPLACE INTO bot_directives (bot_name, directive_key, directive_value, updated_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
`, [this.botName, key, value], (err) => {
if (err) reject(err);
else resolve();
});
});
}
/**
* Get a specific directive
*/
async getDirective(key) {
return new Promise((resolve, reject) => {
this.db.get(`
SELECT directive_value FROM bot_directives
WHERE bot_name = ? AND directive_key = ?
`, [this.botName, key], (err, row) => {
if (err) reject(err);
else resolve(row ? row.directive_value : null);
});
});
}
/**
* Get all directives for this bot
*/
async getAllDirectives() {
return new Promise((resolve, reject) => {
this.db.all(`
SELECT directive_key, directive_value FROM bot_directives
WHERE bot_name = ?
`, [this.botName], (err, rows) => {
if (err) reject(err);
else {
const directives = {};
for (const row of rows) {
directives[row.directive_key] = row.directive_value;
}
resolve(directives);
}
});
});
}
// ========================================
// General Memories
// ========================================
/**
* Set a general memory (not player-specific)
*/
async setGeneralMemory(key, value) {
return new Promise((resolve, reject) => {
this.db.run(`
INSERT OR REPLACE INTO general_memories (bot_name, memory_key, memory_value, updated_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
`, [this.botName, key, value], (err) => {
if (err) reject(err);
else resolve();
});
});
}
/**
* Get a general memory
*/
async getGeneralMemory(key) {
return new Promise((resolve, reject) => {
this.db.get(`
SELECT memory_value FROM general_memories
WHERE bot_name = ? AND memory_key = ?
`, [this.botName, key], (err, row) => {
if (err) reject(err);
else resolve(row ? row.memory_value : null);
});
});
}
/**
* Get all general memories
*/
async getAllGeneralMemories() {
return new Promise((resolve, reject) => {
this.db.all(`
SELECT memory_key, memory_value FROM general_memories
WHERE bot_name = ?
`, [this.botName], (err, rows) => {
if (err) reject(err);
else {
const memories = {};
for (const row of rows) {
memories[row.memory_key] = row.memory_value;
}
resolve(memories);
}
});
});
}
/**
* Get all general memories with timestamps
*/
async getAllGeneralMemoriesWithTimestamps() {
return new Promise((resolve, reject) => {
this.db.all(`
SELECT memory_key, memory_value, updated_at FROM general_memories
WHERE bot_name = ?
ORDER BY updated_at DESC
`, [this.botName], (err, rows) => {
if (err) reject(err);
else resolve(rows);
});
});
}
// ========================================
// Summary Methods (for AI context)
// ========================================
/**
* Get all directives as formatted string with timestamps
*/
async getDirectivesSummary() {
return new Promise((resolve, reject) => {
this.db.all(`
SELECT directive_key, directive_value, updated_at FROM bot_directives
WHERE bot_name = ?
ORDER BY updated_at DESC
`, [this.botName], (err, rows) => {
if (err) reject(err);
else {
if (rows.length === 0) {
resolve(null);
return;
}
let summary = 'Active Directives:\n';
for (const row of rows) {
const timeAgo = this.formatTimeAgo(row.updated_at);
summary += `- ${row.directive_key}: ${row.directive_value} (set ${timeAgo})\n`;
}
resolve(summary.trim());
}
});
});
}
/**
* Format timestamp as relative time string
*/
formatTimeAgo(timestamp) {
if (!timestamp) return 'unknown';
const now = new Date();
const past = new Date(timestamp);
const diffMs = now - past;
const diffSecs = Math.floor(diffMs / 1000);
const diffMins = Math.floor(diffSecs / 60);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffSecs < 60) return 'just now';
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return past.toLocaleDateString();
}
/**
* Get full memory context for prompt injection
*/
async getMemoryContext() {
const directives = await this.getDirectivesSummary();
const generalMemories = await this.getAllGeneralMemoriesWithTimestamps();
let context = '';
if (directives) {
context += directives + '\n\n';
}
if (generalMemories && generalMemories.length > 0) {
context += 'General Memories:\n';
for (const mem of generalMemories) {
const timeAgo = this.formatTimeAgo(mem.updated_at);
context += `- ${mem.memory_key}: ${mem.memory_value} (stored ${timeAgo})\n`;
}
context += '\n';
}
return context || null;
}
/**
* Get memories for specific players (for prompt injection when they're online)
* @param {string[]} playerNames - Array of player names to get memories for
*/
async getPlayerMemoriesForPrompt(playerNames) {
if (!playerNames || playerNames.length === 0) return null;
let context = '';
for (const playerName of playerNames) {
const memories = await this.getAllPlayerMemories(playerName);
if (Object.keys(memories).length > 0) {
context += `Memories about ${playerName}:\n`;
for (const [key, value] of Object.entries(memories)) {
context += `- ${key}: ${value}\n`;
}
context += '\n';
}
}
return context.trim() || null;
}
// ========================================
// Utility Methods
// ========================================
/**
* Close database connection
*/
async close() {
return new Promise((resolve, reject) => {
if (this.db) {
this.db.close((err) => {
if (err) reject(err);
else {
this.db = null;
this.botName = null;
resolve();
}
});
} else {
resolve();
}
});
}
}
// Singleton instance
module.exports = new AIMemoryDB();