Initial commit
This commit is contained in:
commit
5a8a6c1ba3
81 changed files with 39269 additions and 0 deletions
106
cloud-wrapper/src/index.ts
Normal file
106
cloud-wrapper/src/index.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
import morgan from 'morgan';
|
||||
import dotenv from 'dotenv';
|
||||
import http from 'http';
|
||||
import { WebSocketServer } from 'ws';
|
||||
import { BrainyData } from '@soulcraft/brainy';
|
||||
import { setupRoutes } from './routes.js';
|
||||
import { initializeBrainy } from './services/brainyService.js';
|
||||
import { setupWebSocketHandlers } from './websocket.js';
|
||||
import { initializeMCPService } from './services/mcpService.js';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
// Initialize Express app
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
// Middleware
|
||||
app.use(helmet()); // Security headers
|
||||
app.use(cors()); // Enable CORS
|
||||
app.use(morgan('combined')); // Logging
|
||||
app.use(express.json()); // Parse JSON bodies
|
||||
|
||||
// Initialize Brainy
|
||||
let brainyInstance: BrainyData;
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (req, res) => {
|
||||
res.status(200).json({ status: 'ok' });
|
||||
});
|
||||
|
||||
// Setup API routes
|
||||
const apiRouter = setupRoutes();
|
||||
app.use('/api', (req, res, next) => {
|
||||
// Attach brainy instance to request
|
||||
(req as any).brainy = brainyInstance;
|
||||
next();
|
||||
}, apiRouter);
|
||||
|
||||
// Start the server
|
||||
async function startServer() {
|
||||
try {
|
||||
// Initialize Brainy
|
||||
brainyInstance = await initializeBrainy();
|
||||
console.log('Brainy initialized successfully');
|
||||
|
||||
// Create HTTP server
|
||||
const server = http.createServer(app);
|
||||
|
||||
// Create WebSocket server
|
||||
const wss = new WebSocketServer({ server });
|
||||
|
||||
// Setup WebSocket handlers
|
||||
setupWebSocketHandlers(wss, brainyInstance);
|
||||
console.log('WebSocket server initialized');
|
||||
|
||||
// Initialize MCP service
|
||||
const mcpWsPort = process.env.MCP_WS_PORT ? parseInt(process.env.MCP_WS_PORT, 10) : undefined;
|
||||
const mcpRestPort = process.env.MCP_REST_PORT ? parseInt(process.env.MCP_REST_PORT, 10) : undefined;
|
||||
|
||||
if (mcpWsPort || mcpRestPort) {
|
||||
initializeMCPService(brainyInstance, {
|
||||
wsPort: mcpWsPort,
|
||||
restPort: mcpRestPort,
|
||||
enableAuth: process.env.MCP_ENABLE_AUTH === 'true',
|
||||
apiKeys: process.env.MCP_API_KEYS ? process.env.MCP_API_KEYS.split(',') : undefined,
|
||||
rateLimit: process.env.MCP_RATE_LIMIT_REQUESTS ? {
|
||||
windowMs: parseInt(process.env.MCP_RATE_LIMIT_WINDOW_MS || '60000', 10),
|
||||
maxRequests: parseInt(process.env.MCP_RATE_LIMIT_REQUESTS, 10)
|
||||
} : undefined,
|
||||
cors: process.env.MCP_ENABLE_CORS === 'true' ? {} : undefined
|
||||
});
|
||||
console.log('MCP service initialized');
|
||||
}
|
||||
|
||||
// Start HTTP server
|
||||
server.listen(port, () => {
|
||||
console.log(`Server running on port ${port}`);
|
||||
console.log(`WebSocket server running on ws://localhost:${port}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on('SIGTERM', async () => {
|
||||
console.log('SIGTERM received, shutting down gracefully');
|
||||
// Cleanup Brainy resources
|
||||
await brainyInstance?.clear();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('SIGINT received, shutting down gracefully');
|
||||
// Cleanup Brainy resources
|
||||
await brainyInstance?.clear();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Start the server
|
||||
startServer();
|
||||
186
cloud-wrapper/src/routes.ts
Normal file
186
cloud-wrapper/src/routes.ts
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import { Router, Request, Response, NextFunction, RequestHandler } from 'express';
|
||||
import { BrainyData, NounType, VerbType } from '@soulcraft/brainy';
|
||||
|
||||
// Define a custom request type that includes the Brainy instance
|
||||
interface BrainyRequest extends Request {
|
||||
brainy: BrainyData;
|
||||
}
|
||||
|
||||
// Helper function to handle async route handlers
|
||||
const asyncHandler = (fn: (req: BrainyRequest, res: Response, next: NextFunction) => Promise<any>) =>
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
Promise.resolve(fn(req as BrainyRequest, res, next)).catch(next);
|
||||
};
|
||||
|
||||
export function setupRoutes() {
|
||||
const router = Router();
|
||||
|
||||
// Get database status
|
||||
router.get('/status', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const status = await req.brainy.status();
|
||||
res.status(200).json(status);
|
||||
} catch (error) {
|
||||
console.error('Error getting status:', error);
|
||||
res.status(500).json({ error: 'Failed to get database status' });
|
||||
}
|
||||
}));
|
||||
|
||||
// Add a noun (entity)
|
||||
router.post('/nouns', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { text, metadata } = req.body;
|
||||
|
||||
if (!text) {
|
||||
return res.status(400).json({ error: 'Text is required' });
|
||||
}
|
||||
|
||||
const id = await req.brainy.add(text, metadata || {});
|
||||
res.status(201).json({ id });
|
||||
} catch (error) {
|
||||
console.error('Error adding noun:', error);
|
||||
res.status(500).json({ error: 'Failed to add noun' });
|
||||
}
|
||||
}));
|
||||
|
||||
// Get a noun by ID
|
||||
router.get('/nouns/:id', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const noun = await req.brainy.get(id);
|
||||
|
||||
if (!noun) {
|
||||
return res.status(404).json({ error: 'Noun not found' });
|
||||
}
|
||||
|
||||
res.status(200).json(noun);
|
||||
} catch (error) {
|
||||
console.error('Error getting noun:', error);
|
||||
res.status(500).json({ error: 'Failed to get noun' });
|
||||
}
|
||||
}));
|
||||
|
||||
// Update noun metadata
|
||||
router.put('/nouns/:id', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { metadata } = req.body;
|
||||
|
||||
if (!metadata) {
|
||||
return res.status(400).json({ error: 'Metadata is required' });
|
||||
}
|
||||
|
||||
await req.brainy.updateMetadata(id, metadata);
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error updating noun:', error);
|
||||
res.status(500).json({ error: 'Failed to update noun' });
|
||||
}
|
||||
}));
|
||||
|
||||
// Delete a noun
|
||||
router.delete('/nouns/:id', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
await req.brainy.delete(id);
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error deleting noun:', error);
|
||||
res.status(500).json({ error: 'Failed to delete noun' });
|
||||
}
|
||||
}));
|
||||
|
||||
// Search for similar nouns
|
||||
router.post('/search', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { query, limit = 10 } = req.body;
|
||||
|
||||
if (!query) {
|
||||
return res.status(400).json({ error: 'Query is required' });
|
||||
}
|
||||
|
||||
const results = await req.brainy.searchText(query, limit);
|
||||
res.status(200).json(results);
|
||||
} catch (error) {
|
||||
console.error('Error searching:', error);
|
||||
res.status(500).json({ error: 'Failed to search' });
|
||||
}
|
||||
}));
|
||||
|
||||
// Add a verb (relationship)
|
||||
router.post('/verbs', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { sourceId, targetId, metadata } = req.body;
|
||||
|
||||
if (!sourceId || !targetId) {
|
||||
return res.status(400).json({ error: 'Source ID and Target ID are required' });
|
||||
}
|
||||
|
||||
await req.brainy.addVerb(sourceId, targetId, metadata || {});
|
||||
res.status(201).json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error adding verb:', error);
|
||||
res.status(500).json({ error: 'Failed to add verb' });
|
||||
}
|
||||
}));
|
||||
|
||||
// Get all verbs
|
||||
router.get('/verbs', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const verbs = await req.brainy.getAllVerbs();
|
||||
res.status(200).json(verbs);
|
||||
} catch (error) {
|
||||
console.error('Error getting verbs:', error);
|
||||
res.status(500).json({ error: 'Failed to get verbs' });
|
||||
}
|
||||
}));
|
||||
|
||||
// Get verbs by source
|
||||
router.get('/verbs/source/:id', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const verbs = await req.brainy.getVerbsBySource(id);
|
||||
res.status(200).json(verbs);
|
||||
} catch (error) {
|
||||
console.error('Error getting verbs by source:', error);
|
||||
res.status(500).json({ error: 'Failed to get verbs by source' });
|
||||
}
|
||||
}));
|
||||
|
||||
// Get verbs by target
|
||||
router.get('/verbs/target/:id', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const verbs = await req.brainy.getVerbsByTarget(id);
|
||||
res.status(200).json(verbs);
|
||||
} catch (error) {
|
||||
console.error('Error getting verbs by target:', error);
|
||||
res.status(500).json({ error: 'Failed to get verbs by target' });
|
||||
}
|
||||
}));
|
||||
|
||||
// Delete a verb
|
||||
router.delete('/verbs/:id', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
await req.brainy.deleteVerb(id);
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error deleting verb:', error);
|
||||
res.status(500).json({ error: 'Failed to delete verb' });
|
||||
}
|
||||
}));
|
||||
|
||||
// Clear all data
|
||||
router.delete('/clear', asyncHandler(async (req: BrainyRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await req.brainy.clear();
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error clearing data:', error);
|
||||
res.status(500).json({ error: 'Failed to clear data' });
|
||||
}
|
||||
}));
|
||||
|
||||
return router;
|
||||
}
|
||||
97
cloud-wrapper/src/services/brainyService.ts
Normal file
97
cloud-wrapper/src/services/brainyService.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { BrainyData, BrainyDataConfig } from '@soulcraft/brainy'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config()
|
||||
|
||||
/**
|
||||
* Extended configuration interface that includes rootDirectory
|
||||
*/
|
||||
interface ExtendedBrainyDataConfig extends BrainyDataConfig {
|
||||
storage: BrainyDataConfig['storage'] & {
|
||||
rootDirectory?: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the Brainy instance with appropriate configuration
|
||||
* based on environment variables
|
||||
*/
|
||||
export async function initializeBrainy(): Promise<BrainyData> {
|
||||
// Get storage configuration from environment variables
|
||||
const storageType = process.env.STORAGE_TYPE || 'filesystem'
|
||||
|
||||
// Create configuration object
|
||||
const config: ExtendedBrainyDataConfig = {
|
||||
storage: {}
|
||||
}
|
||||
|
||||
// Configure storage based on type
|
||||
switch (storageType) {
|
||||
case 's3':
|
||||
// Configure S3 storage
|
||||
if (process.env.S3_ENDPOINT) {
|
||||
// Use customS3Storage for S3-compatible services with custom endpoints
|
||||
config.storage.customS3Storage = {
|
||||
bucketName: process.env.S3_BUCKET_NAME,
|
||||
accessKeyId: process.env.S3_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
||||
region: process.env.S3_REGION || 'us-east-1',
|
||||
endpoint: process.env.S3_ENDPOINT
|
||||
}
|
||||
} else {
|
||||
// Use standard S3 storage for AWS S3
|
||||
config.storage.s3Storage = {
|
||||
bucketName: process.env.S3_BUCKET_NAME,
|
||||
accessKeyId: process.env.S3_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
||||
region: process.env.S3_REGION || 'us-east-1'
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case 'filesystem':
|
||||
// Configure filesystem storage
|
||||
config.storage.rootDirectory = process.env.STORAGE_ROOT_DIR || './data'
|
||||
break
|
||||
|
||||
case 'memory':
|
||||
// No additional configuration needed for memory storage
|
||||
break
|
||||
|
||||
default:
|
||||
// Default to filesystem storage
|
||||
config.storage.rootDirectory = process.env.STORAGE_ROOT_DIR || './data'
|
||||
break
|
||||
}
|
||||
|
||||
// Note: Universal Sentence Encoder is now the only embedding option
|
||||
// TensorFlow.js is required for embedding to work
|
||||
|
||||
// Configure HNSW index parameters if provided
|
||||
if (process.env.HNSW_M) {
|
||||
config.hnsw = {
|
||||
M: parseInt(process.env.HNSW_M, 10),
|
||||
efConstruction: process.env.HNSW_EF_CONSTRUCTION
|
||||
? parseInt(process.env.HNSW_EF_CONSTRUCTION, 10)
|
||||
: 200,
|
||||
efSearch: process.env.HNSW_EF_SEARCH
|
||||
? parseInt(process.env.HNSW_EF_SEARCH, 10)
|
||||
: 50
|
||||
}
|
||||
}
|
||||
|
||||
// Create and initialize the Brainy instance
|
||||
const brainy = new BrainyData(config)
|
||||
await brainy.init()
|
||||
|
||||
return brainy
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current storage type being used by Brainy
|
||||
*/
|
||||
export async function getStorageType(brainy: BrainyData): Promise<string> {
|
||||
const status = await brainy.status()
|
||||
return status.type || 'unknown'
|
||||
}
|
||||
204
cloud-wrapper/src/services/mcpService.ts
Normal file
204
cloud-wrapper/src/services/mcpService.ts
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
import { WebSocketServer } from 'ws'
|
||||
import express from 'express'
|
||||
import cors from 'cors'
|
||||
import { BrainyData, BrainyMCPService, MCPRequestType } from '@soulcraft/brainy'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
/**
|
||||
* Initialize the MCP service with WebSocket and REST API servers
|
||||
*/
|
||||
export function initializeMCPService(
|
||||
brainy: BrainyData,
|
||||
options: {
|
||||
wsPort?: number
|
||||
restPort?: number
|
||||
enableAuth?: boolean
|
||||
apiKeys?: string[]
|
||||
rateLimit?: {
|
||||
windowMs: number
|
||||
maxRequests: number
|
||||
}
|
||||
cors?: any
|
||||
}
|
||||
) {
|
||||
// Create the MCP service
|
||||
const mcpService = new BrainyMCPService(brainy, options)
|
||||
|
||||
// Start WebSocket server if port is provided
|
||||
if (options.wsPort) {
|
||||
startWebSocketServer(mcpService, options.wsPort)
|
||||
}
|
||||
|
||||
// Start REST server if port is provided
|
||||
if (options.restPort) {
|
||||
startRESTServer(mcpService, options.restPort, options.cors)
|
||||
}
|
||||
|
||||
return mcpService
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a WebSocket server for the MCP service
|
||||
*/
|
||||
function startWebSocketServer(mcpService: BrainyMCPService, port: number) {
|
||||
const wss = new WebSocketServer({ port })
|
||||
|
||||
wss.on('connection', (ws: any) => {
|
||||
ws.on('message', async (message: string) => {
|
||||
try {
|
||||
const request = JSON.parse(message)
|
||||
|
||||
// Handle the request using the MCP service
|
||||
const response = await mcpService.handleMCPRequest(request)
|
||||
|
||||
// Send the response
|
||||
ws.send(JSON.stringify(response))
|
||||
} catch (error) {
|
||||
// Send error response
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
requestId: uuidv4(),
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
console.log(`MCP WebSocket server started on port ${port}`)
|
||||
|
||||
return wss
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a REST server for the MCP service
|
||||
*/
|
||||
function startRESTServer(
|
||||
mcpService: BrainyMCPService,
|
||||
port: number,
|
||||
corsOptions?: any
|
||||
) {
|
||||
const app = express()
|
||||
|
||||
// Parse JSON request bodies
|
||||
app.use(express.json())
|
||||
|
||||
// Enable CORS if configured
|
||||
if (corsOptions) {
|
||||
app.use(cors(corsOptions))
|
||||
}
|
||||
|
||||
// MCP endpoints
|
||||
app.post('/mcp/data', async (req: any, res: any) => {
|
||||
try {
|
||||
const response = await mcpService.handleMCPRequest({
|
||||
...req.body,
|
||||
type: 'DATA_ACCESS'
|
||||
})
|
||||
|
||||
res.json(response)
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
requestId: uuidv4(),
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/mcp/tools', async (req: any, res: any) => {
|
||||
try {
|
||||
const response = await mcpService.handleMCPRequest({
|
||||
...req.body,
|
||||
type: 'TOOL_EXECUTION'
|
||||
})
|
||||
|
||||
res.json(response)
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
requestId: uuidv4(),
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/mcp/system', async (req: any, res: any) => {
|
||||
try {
|
||||
const response = await mcpService.handleMCPRequest({
|
||||
...req.body,
|
||||
type: 'SYSTEM_INFO'
|
||||
})
|
||||
|
||||
res.json(response)
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
requestId: uuidv4(),
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/mcp/auth', async (req: any, res: any) => {
|
||||
try {
|
||||
const response = await mcpService.handleMCPRequest({
|
||||
...req.body,
|
||||
type: 'AUTHENTICATION'
|
||||
})
|
||||
|
||||
res.json(response)
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
requestId: uuidv4(),
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Get available tools
|
||||
app.get('/mcp/tools', async (req: any, res: any) => {
|
||||
try {
|
||||
const response = await mcpService.handleMCPRequest({
|
||||
type: MCPRequestType.SYSTEM_INFO,
|
||||
requestId: uuidv4(),
|
||||
version: '1.0'
|
||||
})
|
||||
|
||||
res.json(response)
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
requestId: uuidv4(),
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Start the server
|
||||
const server = app.listen(port, () => {
|
||||
console.log(`MCP REST API server started on port ${port}`)
|
||||
})
|
||||
|
||||
return server
|
||||
}
|
||||
433
cloud-wrapper/src/websocket.ts
Normal file
433
cloud-wrapper/src/websocket.ts
Normal file
|
|
@ -0,0 +1,433 @@
|
|||
import { WebSocketServer, WebSocket } from 'ws';
|
||||
import { BrainyData } from '@soulcraft/brainy';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
// Define message types
|
||||
enum MessageType {
|
||||
STATUS = 'status',
|
||||
ADD_NOUN = 'addNoun',
|
||||
GET_NOUN = 'getNoun',
|
||||
UPDATE_NOUN = 'updateNoun',
|
||||
DELETE_NOUN = 'deleteNoun',
|
||||
SEARCH = 'search',
|
||||
ADD_VERB = 'addVerb',
|
||||
GET_VERBS = 'getVerbs',
|
||||
GET_VERBS_BY_SOURCE = 'getVerbsBySource',
|
||||
GET_VERBS_BY_TARGET = 'getVerbsByTarget',
|
||||
DELETE_VERB = 'deleteVerb',
|
||||
CLEAR = 'clear',
|
||||
SUBSCRIBE = 'subscribe',
|
||||
UNSUBSCRIBE = 'unsubscribe',
|
||||
ERROR = 'error'
|
||||
}
|
||||
|
||||
// Define subscription types
|
||||
enum SubscriptionType {
|
||||
NOUNS = 'nouns',
|
||||
VERBS = 'verbs',
|
||||
SEARCH_RESULTS = 'searchResults'
|
||||
}
|
||||
|
||||
// Define message interface
|
||||
interface WebSocketMessage {
|
||||
type: MessageType;
|
||||
id?: string;
|
||||
payload?: any;
|
||||
}
|
||||
|
||||
// Define client interface
|
||||
interface WebSocketClient {
|
||||
id: string;
|
||||
socket: WebSocket;
|
||||
subscriptions: Set<SubscriptionType>;
|
||||
}
|
||||
|
||||
// Store connected clients
|
||||
const clients: Map<string, WebSocketClient> = new Map();
|
||||
|
||||
// Setup WebSocket handlers
|
||||
export function setupWebSocketHandlers(wss: WebSocketServer, brainy: BrainyData) {
|
||||
// Handle new connections
|
||||
wss.on('connection', (socket: WebSocket) => {
|
||||
const clientId = uuidv4();
|
||||
|
||||
// Create client object
|
||||
const client: WebSocketClient = {
|
||||
id: clientId,
|
||||
socket,
|
||||
subscriptions: new Set()
|
||||
};
|
||||
|
||||
// Store client
|
||||
clients.set(clientId, client);
|
||||
|
||||
console.log(`WebSocket client connected: ${clientId}`);
|
||||
|
||||
// Send welcome message
|
||||
sendMessage(socket, {
|
||||
type: MessageType.STATUS,
|
||||
id: uuidv4(),
|
||||
payload: {
|
||||
clientId,
|
||||
message: 'Connected to Brainy WebSocket API',
|
||||
status: 'connected'
|
||||
}
|
||||
});
|
||||
|
||||
// Handle messages
|
||||
socket.on('message', async (data: WebSocket.Data) => {
|
||||
try {
|
||||
const message: WebSocketMessage = JSON.parse(data.toString());
|
||||
|
||||
// Ensure message has an ID
|
||||
const messageId = message.id || uuidv4();
|
||||
|
||||
console.log(`Received message: ${message.type} (${messageId})`);
|
||||
|
||||
// Process message based on type
|
||||
await processMessage(client, message, messageId, brainy);
|
||||
} catch (error) {
|
||||
console.error('Error processing WebSocket message:', error);
|
||||
sendMessage(socket, {
|
||||
type: MessageType.ERROR,
|
||||
id: uuidv4(),
|
||||
payload: {
|
||||
message: 'Invalid message format',
|
||||
error: (error as Error).message
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Handle disconnection
|
||||
socket.on('close', () => {
|
||||
// Remove client
|
||||
clients.delete(clientId);
|
||||
console.log(`WebSocket client disconnected: ${clientId}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Process incoming messages
|
||||
async function processMessage(
|
||||
client: WebSocketClient,
|
||||
message: WebSocketMessage,
|
||||
messageId: string,
|
||||
brainy: BrainyData
|
||||
) {
|
||||
const { socket } = client;
|
||||
|
||||
try {
|
||||
switch (message.type) {
|
||||
case MessageType.STATUS:
|
||||
// Return database status
|
||||
const status = await brainy.status();
|
||||
sendMessage(socket, {
|
||||
type: MessageType.STATUS,
|
||||
id: messageId,
|
||||
payload: status
|
||||
});
|
||||
break;
|
||||
|
||||
case MessageType.ADD_NOUN:
|
||||
// Add a noun
|
||||
if (!message.payload?.text) {
|
||||
throw new Error('Text is required');
|
||||
}
|
||||
|
||||
const nounId = await brainy.add(
|
||||
message.payload.text,
|
||||
message.payload.metadata || {}
|
||||
);
|
||||
|
||||
sendMessage(socket, {
|
||||
type: MessageType.ADD_NOUN,
|
||||
id: messageId,
|
||||
payload: { id: nounId }
|
||||
});
|
||||
|
||||
// Notify subscribers
|
||||
notifySubscribers(SubscriptionType.NOUNS, {
|
||||
type: 'added',
|
||||
id: nounId,
|
||||
data: await brainy.get(nounId)
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case MessageType.GET_NOUN:
|
||||
// Get a noun by ID
|
||||
if (!message.payload?.id) {
|
||||
throw new Error('Noun ID is required');
|
||||
}
|
||||
|
||||
const noun = await brainy.get(message.payload.id);
|
||||
|
||||
if (!noun) {
|
||||
throw new Error('Noun not found');
|
||||
}
|
||||
|
||||
sendMessage(socket, {
|
||||
type: MessageType.GET_NOUN,
|
||||
id: messageId,
|
||||
payload: noun
|
||||
});
|
||||
break;
|
||||
|
||||
case MessageType.UPDATE_NOUN:
|
||||
// Update noun metadata
|
||||
if (!message.payload?.id || !message.payload?.metadata) {
|
||||
throw new Error('Noun ID and metadata are required');
|
||||
}
|
||||
|
||||
await brainy.updateMetadata(message.payload.id, message.payload.metadata);
|
||||
|
||||
sendMessage(socket, {
|
||||
type: MessageType.UPDATE_NOUN,
|
||||
id: messageId,
|
||||
payload: { success: true }
|
||||
});
|
||||
|
||||
// Notify subscribers
|
||||
notifySubscribers(SubscriptionType.NOUNS, {
|
||||
type: 'updated',
|
||||
id: message.payload.id,
|
||||
data: await brainy.get(message.payload.id)
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case MessageType.DELETE_NOUN:
|
||||
// Delete a noun
|
||||
if (!message.payload?.id) {
|
||||
throw new Error('Noun ID is required');
|
||||
}
|
||||
|
||||
await brainy.delete(message.payload.id);
|
||||
|
||||
sendMessage(socket, {
|
||||
type: MessageType.DELETE_NOUN,
|
||||
id: messageId,
|
||||
payload: { success: true }
|
||||
});
|
||||
|
||||
// Notify subscribers
|
||||
notifySubscribers(SubscriptionType.NOUNS, {
|
||||
type: 'deleted',
|
||||
id: message.payload.id
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case MessageType.SEARCH:
|
||||
// Search for similar nouns
|
||||
if (!message.payload?.query) {
|
||||
throw new Error('Query is required');
|
||||
}
|
||||
|
||||
const limit = message.payload.limit || 10;
|
||||
const results = await brainy.searchText(message.payload.query, limit);
|
||||
|
||||
sendMessage(socket, {
|
||||
type: MessageType.SEARCH,
|
||||
id: messageId,
|
||||
payload: results
|
||||
});
|
||||
|
||||
// Notify subscribers
|
||||
notifySubscribers(SubscriptionType.SEARCH_RESULTS, {
|
||||
type: 'search',
|
||||
query: message.payload.query,
|
||||
results
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case MessageType.ADD_VERB:
|
||||
// Add a verb (relationship)
|
||||
if (!message.payload?.sourceId || !message.payload?.targetId) {
|
||||
throw new Error('Source ID and Target ID are required');
|
||||
}
|
||||
|
||||
await brainy.addVerb(
|
||||
message.payload.sourceId,
|
||||
message.payload.targetId,
|
||||
message.payload.metadata || {}
|
||||
);
|
||||
|
||||
sendMessage(socket, {
|
||||
type: MessageType.ADD_VERB,
|
||||
id: messageId,
|
||||
payload: { success: true }
|
||||
});
|
||||
|
||||
// Notify subscribers
|
||||
notifySubscribers(SubscriptionType.VERBS, {
|
||||
type: 'added',
|
||||
sourceId: message.payload.sourceId,
|
||||
targetId: message.payload.targetId,
|
||||
metadata: message.payload.metadata
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case MessageType.GET_VERBS:
|
||||
// Get all verbs
|
||||
const verbs = await brainy.getAllVerbs();
|
||||
|
||||
sendMessage(socket, {
|
||||
type: MessageType.GET_VERBS,
|
||||
id: messageId,
|
||||
payload: verbs
|
||||
});
|
||||
break;
|
||||
|
||||
case MessageType.GET_VERBS_BY_SOURCE:
|
||||
// Get verbs by source
|
||||
if (!message.payload?.id) {
|
||||
throw new Error('Source ID is required');
|
||||
}
|
||||
|
||||
const sourceVerbs = await brainy.getVerbsBySource(message.payload.id);
|
||||
|
||||
sendMessage(socket, {
|
||||
type: MessageType.GET_VERBS_BY_SOURCE,
|
||||
id: messageId,
|
||||
payload: sourceVerbs
|
||||
});
|
||||
break;
|
||||
|
||||
case MessageType.GET_VERBS_BY_TARGET:
|
||||
// Get verbs by target
|
||||
if (!message.payload?.id) {
|
||||
throw new Error('Target ID is required');
|
||||
}
|
||||
|
||||
const targetVerbs = await brainy.getVerbsByTarget(message.payload.id);
|
||||
|
||||
sendMessage(socket, {
|
||||
type: MessageType.GET_VERBS_BY_TARGET,
|
||||
id: messageId,
|
||||
payload: targetVerbs
|
||||
});
|
||||
break;
|
||||
|
||||
case MessageType.DELETE_VERB:
|
||||
// Delete a verb
|
||||
if (!message.payload?.id) {
|
||||
throw new Error('Verb ID is required');
|
||||
}
|
||||
|
||||
await brainy.deleteVerb(message.payload.id);
|
||||
|
||||
sendMessage(socket, {
|
||||
type: MessageType.DELETE_VERB,
|
||||
id: messageId,
|
||||
payload: { success: true }
|
||||
});
|
||||
|
||||
// Notify subscribers
|
||||
notifySubscribers(SubscriptionType.VERBS, {
|
||||
type: 'deleted',
|
||||
id: message.payload.id
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case MessageType.CLEAR:
|
||||
// Clear all data
|
||||
await brainy.clear();
|
||||
|
||||
sendMessage(socket, {
|
||||
type: MessageType.CLEAR,
|
||||
id: messageId,
|
||||
payload: { success: true }
|
||||
});
|
||||
|
||||
// Notify all subscribers
|
||||
notifySubscribers(SubscriptionType.NOUNS, { type: 'cleared' });
|
||||
notifySubscribers(SubscriptionType.VERBS, { type: 'cleared' });
|
||||
|
||||
break;
|
||||
|
||||
case MessageType.SUBSCRIBE:
|
||||
// Subscribe to events
|
||||
if (!message.payload?.type) {
|
||||
throw new Error('Subscription type is required');
|
||||
}
|
||||
|
||||
const subscriptionType = message.payload.type as SubscriptionType;
|
||||
|
||||
// Add subscription
|
||||
client.subscriptions.add(subscriptionType);
|
||||
|
||||
sendMessage(socket, {
|
||||
type: MessageType.SUBSCRIBE,
|
||||
id: messageId,
|
||||
payload: {
|
||||
success: true,
|
||||
type: subscriptionType
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case MessageType.UNSUBSCRIBE:
|
||||
// Unsubscribe from events
|
||||
if (!message.payload?.type) {
|
||||
throw new Error('Subscription type is required');
|
||||
}
|
||||
|
||||
const unsubscribeType = message.payload.type as SubscriptionType;
|
||||
|
||||
// Remove subscription
|
||||
client.subscriptions.delete(unsubscribeType);
|
||||
|
||||
sendMessage(socket, {
|
||||
type: MessageType.UNSUBSCRIBE,
|
||||
id: messageId,
|
||||
payload: {
|
||||
success: true,
|
||||
type: unsubscribeType
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown message type: ${message.type}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing message ${message.type}:`, error);
|
||||
|
||||
sendMessage(socket, {
|
||||
type: MessageType.ERROR,
|
||||
id: messageId,
|
||||
payload: {
|
||||
originalType: message.type,
|
||||
message: 'Error processing message',
|
||||
error: (error as Error).message
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Send a message to a client
|
||||
function sendMessage(socket: WebSocket, message: WebSocketMessage) {
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
|
||||
// Notify subscribers of events
|
||||
function notifySubscribers(type: SubscriptionType, data: any) {
|
||||
for (const client of clients.values()) {
|
||||
if (client.subscriptions.has(type)) {
|
||||
sendMessage(client.socket, {
|
||||
type: MessageType.SUBSCRIBE,
|
||||
payload: {
|
||||
type,
|
||||
data
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue