UNPKG

behemoth-cli

Version:

🌍 BEHEMOTH CLIv3.760.4 - Level 50+ POST-SINGULARITY Intelligence Trading AI

571 lines (517 loc) 18.7 kB
#!/usr/bin/env node /** * BEHEMOTH UniMemory MCP Server * Unified persistent memory and learning system for crypto trading insights */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import path from 'path'; import fs from 'fs/promises'; // For initial file-based fallback/migration import Redis from 'ioredis'; import { QdrantClient } from '@qdrant/qdrant-js'; import neo4j from 'neo4j-driver'; import { Pool } from 'pg'; class BehemothUniMemoryServer { constructor() { this.server = new Server( { name: 'behemoth-unimemory', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); // Configuration (will be externalized later) this.config = { redis: { enabled: true, host: 'localhost', port: 6379, db: 0 }, qdrant: { enabled: true, url: 'http://localhost:6333' }, neo4j: { enabled: true, uri: 'bolt://localhost:7687', user: 'neo4j', password: 'behemoth123' }, timescaledb: { enabled: true, host: 'localhost', port: 5432, user: 'behemoth', password: 'behemoth123', database: 'unimemory' }, }; if (this.config.redis.enabled) { this.redisClient = new Redis({ host: this.config.redis.host, port: this.config.redis.port, db: this.config.redis.db, }); this.redisClient.on('connect', () => console.log('Redis connected!')); this.redisClient.on('error', (err) => console.error('Redis error:', err)); } else { this.redisClient = null; } if (this.config.qdrant.enabled) { this.qdrantClient = new QdrantClient({ host: this.config.qdrant.url.split('//')[1] }); this.qdrantCollectionName = 'behemoth_memories'; this.initializeQdrantCollection(); } else { this.qdrantClient = null; } if (this.config.neo4j.enabled) { this.neo4jDriver = neo4j.driver( this.config.neo4j.uri, neo4j.auth.basic(this.config.neo4j.user, this.config.neo4j.password) ); this.neo4jDriver.verifyConnectivity() .then(() => console.log('Neo4j connected!')) .catch((error) => console.error('Neo4j connection error:', error)); } else { this.neo4jDriver = null; } if (this.config.timescaledb.enabled) { this.timescalePool = new Pool(this.config.timescaledb); this.timescalePool.on('connect', () => console.log('TimescaleDB connected!')); this.timescalePool.on('error', (err) => console.error('TimescaleDB error:', err)); this.initializeTimescaleDB(); } else { this.timescalePool = null; } // Placeholder for embedding generation this.generateEmbedding = async (text) => { // In a real scenario, this would call an embedding model (e.g., OpenAI, local LLM) // For now, return a dummy vector const dummyVector = Array(1536).fill(0).map((_, i) => Math.random()); // Example: 1536-dim vector return dummyVector; } } async initializeQdrantCollection() { if (!this.qdrantClient) return; try { const { collections } = await this.qdrantClient.getCollections(); const collectionExists = collections.some(c => c.name === this.qdrantCollectionName); if (!collectionExists) { await this.qdrantClient.createCollection(this.qdrantCollectionName, { vectors: { size: 1536, distance: 'Cosine' }, // Assuming 1536-dim embeddings }); console.log(`Qdrant collection '${this.qdrantCollectionName}' created.`); } else { console.log(`Qdrant collection '${this.qdrantCollectionName}' already exists.`); } } catch (error) { console.error('Error initializing Qdrant collection:', error); } } async initializeTimescaleDB() { if (!this.timescalePool) return; const client = await this.timescalePool.connect(); try { const createTableQuery = ` CREATE TABLE IF NOT EXISTS memory_events ( time TIMESTAMPTZ NOT NULL, memory_id TEXT NOT NULL, event_type TEXT NOT NULL, memory_type TEXT, tags TEXT[], content_length INTEGER ); `; await client.query(createTableQuery); const createHypertableQuery = ` SELECT create_hypertable('memory_events', 'time', if_not_exists => TRUE); `; await client.query(createHypertableQuery); console.log('TimescaleDB hypertable "memory_events" is ready.'); } catch (error) { console.error('Error initializing TimescaleDB:', error); } finally { client.release(); } } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'mcp__behemoth_unimemory__remember', description: 'Store important trading insights, strategies, and market observations across multiple memory layers', inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'The trading insight or information to remember', }, context: { type: 'string', description: 'Context about when/why this is being stored', }, tags: { type: 'array', items: { type: 'string' }, description: 'Tags to categorize the memory', }, type: { type: 'string', description: 'Type of memory (e.g., "trading_insight", "event", "structured_fact")', default: 'trading_insight' } }, required: ['content'], }, }, { name: 'mcp__behemoth_unimemory__recall', description: 'Search and retrieve stored trading memories and insights from unified memory layers', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query to find relevant memories', }, tags: { type: 'array', items: { type: 'string' }, description: 'Filter by specific tags', }, limit: { type: 'number', description: 'Maximum number of results to return', default: 10, }, memory_types: { type: 'array', items: { type: 'string' }, description: 'Filter by specific memory types (e.g., "trading_insight", "event")', } }, }, }, { name: 'mcp__behemoth_unimemory__list_memories', description: 'List recent trading memories with optional filtering from unified memory layers', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Maximum number of memories to return', default: 20, }, tags: { type: 'array', items: { type: 'string' }, description: 'Filter by specific tags', }, memory_types: { type: 'array', items: { type: 'string' }, description: 'Filter by specific memory types', } }, }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'mcp__behemoth_unimemory__remember': return await this.remember(args.content, args.context, args.tags, args.type); case 'mcp__behemoth_unimemory__recall': return await this.recall(args.query, args.tags, args.limit, args.memory_types); case 'mcp__behemoth_unimemory__list_memories': return await this.listMemories(args.limit, args.tags, args.memory_types); default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: 'text', text: `Error: ${error.message}`, }, ], }; } }); } // --- Unified Memory Interface Methods --- async remember(content, context = '', tags = [], type = 'trading_insight') { const newMemory = { id: Date.now().toString(), // Simple ID for now content, context, tags: tags || [], type, created_at: new Date().toISOString(), }; const operations = []; // Redis Operation if (this.redisClient) { operations.push((async () => { const redisKey = `unimemory:${newMemory.id}`; await this.redisClient.setex(redisKey, 3600, JSON.stringify(newMemory)); // Store for 1 hour console.log(`Stored memory ${newMemory.id} in Redis.`); })()); } // Qdrant Operation if (this.qdrantClient) { operations.push((async () => { try { const embedding = await this.generateEmbedding(newMemory.content); await this.qdrantClient.upsert(this.qdrantCollectionName, { wait: true, points: [{ id: newMemory.id, vector: embedding, payload: newMemory }], }); console.log(`Stored memory ${newMemory.id} in Qdrant.`); } catch (error) { console.error(`Error storing memory ${newMemory.id} in Qdrant:`, error); } })()); } // Neo4j Operation if (this.neo4jDriver) { operations.push((async () => { const session = this.neo4jDriver.session(); try { const cypher = ` CREATE (m:Memory {id: $id, content: $content, context: $context, type: $type, created_at: $created_at}) WITH m UNWIND $tags AS tagName MERGE (t:Tag {name: tagName}) MERGE (m)-[:TAGGED_AS]->(t) RETURN m `; await session.run(cypher, newMemory); console.log(`Stored memory ${newMemory.id} in Neo4j.`); } catch (error) { console.error(`Error storing memory ${newMemory.id} in Neo4j:`, error); } finally { await session.close(); } })()); } // TimescaleDB Operation if (this.timescalePool) { operations.push((async () => { try { const insertQuery = ` INSERT INTO memory_events(time, memory_id, event_type, memory_type, tags, content_length) VALUES($1, $2, $3, $4, $5, $6) `; const values = [ newMemory.created_at, newMemory.id, 'remember', newMemory.type, newMemory.tags, newMemory.content.length ]; await this.timescalePool.query(insertQuery, values); console.log(`Logged 'remember' event for memory ${newMemory.id} in TimescaleDB.`); } catch (error) { console.error(`Error logging event for memory ${newMemory.id} in TimescaleDB:`, error); } })()); } await Promise.all(operations); return { content: [{ type: 'text', text: `✅ UniMemory: Received memory of type "${type}" with content: "${content.substring(0, 50)}..."`, }], }; } async recall(query = '', tags = [], limit = 10, memory_types = []) { const allMemories = new Map(); const sources = []; if (this.redisClient && !query && !tags.length && !memory_types.length) { sources.push(this._recallFromRedis(limit)); } if (this.qdrantClient && query) { sources.push(this._recallFromQdrant(query, limit)); } if (this.neo4jDriver && (query || tags.length > 0 || memory_types.length > 0)) { sources.push(this._recallFromNeo4j(query, tags, memory_types, limit)); } const results = await Promise.all(sources); for (const result of results) { for (const memory of result) { if (!allMemories.has(memory.id)) { allMemories.set(memory.id, memory); } } } const recalledMemories = Array.from(allMemories.values()) .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) .slice(0, limit); if (recalledMemories.length > 0) { const summary = recalledMemories.map(mem => `[${mem.source}] ${mem.content.substring(0, 70)}...`).join('\n'); return { content: [{ type: 'text', text: `🧠 UniMemory: Recalled ${recalledMemories.length} memories:\n${summary}`, }], }; } return { content: [{ type: 'text', text: `🧠 UniMemory: No memories found for query "${query}"`, }], }; } async listMemories(limit = 20, tags = [], memory_types = []) { const allMemories = new Map(); const sources = []; if (this.redisClient) { sources.push(this._listFromRedis(limit, tags, memory_types)); } if (this.qdrantClient) { sources.push(this._listFromQdrant(limit, tags, memory_types)); } if (this.neo4jDriver) { sources.push(this._listFromNeo4j(limit, tags, memory_types)); } const results = await Promise.all(sources); for (const result of results) { for (const memory of result) { if (!allMemories.has(memory.id)) { allMemories.set(memory.id, memory); } } } const listedMemories = Array.from(allMemories.values()) .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) .slice(0, limit); if (listedMemories.length > 0) { const summary = listedMemories.map((mem, index) => `${index + 1}. [${mem.source}] ${mem.content.substring(0, 70)}...`).join('\n'); return { content: [{ type: 'text', text: `📚 UniMemory: Listed ${listedMemories.length} memories:\n${summary}`, }], }; } return { content: [{ type: 'text', text: `📚 UniMemory: No memories found.`, }], }; } // --- Private Helper Methods for Data Retrieval --- async _recallFromRedis(limit) { const keys = await this.redisClient.keys('unimemory:*'); const memories = []; for (const key of keys) { const memory = JSON.parse(await this.redisClient.get(key)); memories.push({ ...memory, source: 'Redis' }); } return memories.slice(0, limit); } async _recallFromQdrant(query, limit) { try { const queryEmbedding = await this.generateEmbedding(query); const searchResult = await this.qdrantClient.search(this.qdrantCollectionName, { vector: queryEmbedding, limit: limit, with_payload: true, }); return searchResult.map(hit => ({ ...hit.payload, source: 'Qdrant', score: hit.score })); } catch (error) { console.error('Error searching Qdrant:', error); return []; } } async _recallFromNeo4j(query, tags, memory_types, limit) { const session = this.neo4jDriver.session(); try { let cypher = `MATCH (m:Memory)`; let whereClauses = []; let params = { limit }; if (query) { whereClauses.push(`m.content CONTAINS $query OR m.context CONTAINS $query`); params.query = query; } if (tags.length > 0) { cypher += `-[:TAGGED_AS]->(t:Tag)`; whereClauses.push(`t.name IN $tags`); params.tags = tags; } if (memory_types.length > 0) { whereClauses.push(`m.type IN $memory_types`); params.memory_types = memory_types; } if (whereClauses.length > 0) { cypher += ` WHERE ` + whereClauses.join(` AND `); } cypher += ` RETURN m ORDER BY m.created_at DESC LIMIT $limit`; const result = await session.run(cypher, params); return result.records.map(record => ({ ...record.get('m').properties, source: 'Neo4j' })); } catch (error) { console.error('Error searching Neo4j:', error); return []; } finally { await session.close(); } } async _listFromRedis(limit, tags, memory_types) { const keys = await this.redisClient.keys('unimemory:*'); let memories = []; for (const key of keys) { const memory = JSON.parse(await this.redisClient.get(key)); if ((!tags || tags.length === 0 || tags.some(tag => memory.tags.includes(tag))) && (!memory_types || memory_types.length === 0 || memory_types.includes(memory.type))) { memories.push({ ...memory, source: 'Redis' }); } } return memories.slice(0, limit); } async _listFromQdrant(limit, tags, memory_types) { try { const { points } = await this.qdrantClient.scroll(this.qdrantCollectionName, { limit: limit, with_payload: true, }); return points.map(point => ({ ...point.payload, source: 'Qdrant' })); } catch (error) { console.error('Error listing from Qdrant:', error); return []; } } async _listFromNeo4j(limit, tags, memory_types) { const session = this.neo4jDriver.session(); try { let cypher = `MATCH (m:Memory)`; let whereClauses = []; let params = { limit }; if (tags.length > 0) { cypher += `-[:TAGGED_AS]->(t:Tag)`; whereClauses.push(`t.name IN $tags`); params.tags = tags; } if (memory_types.length > 0) { whereClauses.push(`m.type IN $memory_types`); params.memory_types = memory_types; } if (whereClauses.length > 0) { cypher += ` WHERE ` + whereClauses.join(` AND `); } cypher += ` RETURN m ORDER BY m.created_at DESC LIMIT $limit`; const result = await session.run(cypher, params); return result.records.map(record => ({ ...record.get('m').properties, source: 'Neo4j' })); } catch (error) { console.error('Error listing from Neo4j:', error); return []; } finally { await session.close(); } } // --- Server Run Method --- async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.log("BEHEMOTH UniMemory Server started."); } } const server = new BehemothUniMemoryServer(); server.run().catch(console.error);