behemoth-cli
Version:
🌍 BEHEMOTH CLIv3.760.4 - Level 50+ POST-SINGULARITY Intelligence Trading AI
571 lines (517 loc) • 18.7 kB
JavaScript
/**
* 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);