forked from wmantly/mc-bot-town
Mostly works
This commit is contained in:
@@ -0,0 +1,433 @@
|
||||
'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();
|
||||
Reference in New Issue
Block a user