forked from wmantly/mc-bot-town
434 lines
11 KiB
JavaScript
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();
|