From e00f1a92ce3989ee868ff9fe6f7eaf9ea5190036 Mon Sep 17 00:00:00 2001 From: David Snelling Date: Fri, 6 Jun 2025 09:09:07 -0700 Subject: [PATCH] feat: add WebSocket API, REST API routes, and server setup for Brainy Implemented a WebSocket API for real-time communication and REST API routes for CRUD operations on nouns and verbs. Added a server initialization script with WebSocket server integration and improved application structure with modularized routing and services. --- cloud-wrapper/.env.example | 24 ++ cloud-wrapper/scripts/deploy-aws.js | 205 +++++++++ cloud-wrapper/scripts/deploy-cloudflare.js | 245 +++++++++++ cloud-wrapper/scripts/deploy-gcp.js | 196 +++++++++ cloud-wrapper/src/index.ts | 85 ++++ cloud-wrapper/src/routes.ts | 180 ++++++++ cloud-wrapper/src/services/brainyService.ts | 80 ++++ cloud-wrapper/src/websocket.ts | 433 ++++++++++++++++++++ src/brainyData.ts | 170 ++++---- 9 files changed, 1533 insertions(+), 85 deletions(-) create mode 100644 cloud-wrapper/.env.example create mode 100644 cloud-wrapper/scripts/deploy-aws.js create mode 100644 cloud-wrapper/scripts/deploy-cloudflare.js create mode 100644 cloud-wrapper/scripts/deploy-gcp.js create mode 100644 cloud-wrapper/src/index.ts create mode 100644 cloud-wrapper/src/routes.ts create mode 100644 cloud-wrapper/src/services/brainyService.ts create mode 100644 cloud-wrapper/src/websocket.ts diff --git a/cloud-wrapper/.env.example b/cloud-wrapper/.env.example new file mode 100644 index 00000000..c7614644 --- /dev/null +++ b/cloud-wrapper/.env.example @@ -0,0 +1,24 @@ +# Server configuration +PORT=3000 + +# Storage configuration +# Options: 'filesystem', 'memory', 's3' +STORAGE_TYPE=filesystem +STORAGE_ROOT_DIR=./data + +# S3 Storage configuration (only used if STORAGE_TYPE=s3) +S3_BUCKET_NAME=your-bucket-name +S3_ACCESS_KEY_ID=your-access-key +S3_SECRET_ACCESS_KEY=your-secret-key +S3_REGION=us-east-1 +# Optional: For custom S3-compatible services like MinIO, DigitalOcean Spaces, etc. +# S3_ENDPOINT=https://your-custom-endpoint + +# Embedding configuration +# Set to 'true' to use simple embedding (faster but less accurate) +USE_SIMPLE_EMBEDDING=false + +# HNSW index configuration (optional) +# HNSW_M=16 # Max connections per node +# HNSW_EF_CONSTRUCTION=200 # Construction candidate list size +# HNSW_EF_SEARCH=50 # Search candidate list size diff --git a/cloud-wrapper/scripts/deploy-aws.js b/cloud-wrapper/scripts/deploy-aws.js new file mode 100644 index 00000000..57a0ec50 --- /dev/null +++ b/cloud-wrapper/scripts/deploy-aws.js @@ -0,0 +1,205 @@ +#!/usr/bin/env node + +/** + * AWS Deployment Script for Brainy Cloud Wrapper + * + * This script helps deploy the Brainy cloud wrapper to AWS Lambda and API Gateway. + * It uses the AWS SDK to create and configure the necessary resources. + * + * Prerequisites: + * - AWS CLI installed and configured with appropriate credentials + * - Node.js 23.11.0 or higher + * - Brainy cloud wrapper built (npm run build) + */ + +import { execSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import dotenv from 'dotenv'; + +// Load environment variables +dotenv.config(); + +// Configuration +const config = { + region: process.env.AWS_REGION || 'us-east-1', + functionName: process.env.AWS_FUNCTION_NAME || 'brainy-cloud-service', + s3Bucket: process.env.S3_BUCKET_NAME, + s3Key: process.env.S3_ACCESS_KEY_ID, + s3Secret: process.env.S3_SECRET_ACCESS_KEY, + apiGatewayName: process.env.AWS_API_GATEWAY_NAME || 'brainy-api', + stageName: process.env.AWS_STAGE_NAME || 'prod' +}; + +// Validate configuration +if (!config.s3Bucket && process.env.STORAGE_TYPE === 's3') { + console.error('Error: S3 bucket name is required when using S3 storage'); + process.exit(1); +} + +// Create deployment package +function createDeploymentPackage() { + console.log('Creating deployment package...'); + + try { + // Create a temporary directory for the deployment package + if (!fs.existsSync('deploy')) { + fs.mkdirSync('deploy'); + } + + // Copy necessary files + execSync('cp -r dist deploy/'); + execSync('cp package.json deploy/'); + execSync('cp .env deploy/'); + + // Create a zip file + execSync('cd deploy && zip -r ../deployment.zip .'); + + console.log('Deployment package created successfully'); + } catch (error) { + console.error('Error creating deployment package:', error); + process.exit(1); + } +} + +// Deploy to AWS Lambda +function deployToLambda() { + console.log('Deploying to AWS Lambda...'); + + try { + // Check if function exists + try { + execSync(`aws lambda get-function --function-name ${config.functionName} --region ${config.region}`); + + // Update existing function + execSync(`aws lambda update-function-code --function-name ${config.functionName} --zip-file fileb://deployment.zip --region ${config.region}`); + console.log(`Lambda function ${config.functionName} updated successfully`); + } catch (error) { + // Create new function + execSync(`aws lambda create-function --function-name ${config.functionName} --runtime nodejs20.x --handler dist/index.handler --zip-file fileb://deployment.zip --role arn:aws:iam::${process.env.AWS_ACCOUNT_ID}:role/lambda-basic-execution --region ${config.region}`); + console.log(`Lambda function ${config.functionName} created successfully`); + } + + // Configure environment variables + const envVars = { + Variables: { + NODE_ENV: 'production', + STORAGE_TYPE: process.env.STORAGE_TYPE || 'filesystem', + S3_BUCKET_NAME: config.s3Bucket, + S3_ACCESS_KEY_ID: config.s3Key, + S3_SECRET_ACCESS_KEY: config.s3Secret, + S3_REGION: config.region + } + }; + + execSync(`aws lambda update-function-configuration --function-name ${config.functionName} --environment '${JSON.stringify(envVars)}' --region ${config.region}`); + console.log('Lambda function environment variables configured'); + + } catch (error) { + console.error('Error deploying to AWS Lambda:', error); + process.exit(1); + } +} + +// Create API Gateway +function createApiGateway() { + console.log('Creating API Gateway...'); + + try { + // Check if API Gateway exists + let apiId; + try { + const result = execSync(`aws apigateway get-rest-apis --region ${config.region}`).toString(); + const apis = JSON.parse(result).items; + const api = apis.find(api => api.name === config.apiGatewayName); + + if (api) { + apiId = api.id; + console.log(`Using existing API Gateway: ${apiId}`); + } else { + throw new Error('API Gateway not found'); + } + } catch (error) { + // Create new API Gateway + const result = execSync(`aws apigateway create-rest-api --name ${config.apiGatewayName} --region ${config.region}`).toString(); + apiId = JSON.parse(result).id; + console.log(`API Gateway created: ${apiId}`); + } + + // Get root resource ID + const resourcesResult = execSync(`aws apigateway get-resources --rest-api-id ${apiId} --region ${config.region}`).toString(); + const rootResourceId = JSON.parse(resourcesResult).items.find(resource => resource.path === '/').id; + + // Create proxy resource + let proxyResourceId; + try { + const proxyResource = JSON.parse(resourcesResult).items.find(resource => resource.path === '/{proxy+}'); + if (proxyResource) { + proxyResourceId = proxyResource.id; + } else { + throw new Error('Proxy resource not found'); + } + } catch (error) { + const proxyResult = execSync(`aws apigateway create-resource --rest-api-id ${apiId} --parent-id ${rootResourceId} --path-part {proxy+} --region ${config.region}`).toString(); + proxyResourceId = JSON.parse(proxyResult).id; + } + + // Create ANY method + try { + execSync(`aws apigateway put-method --rest-api-id ${apiId} --resource-id ${proxyResourceId} --http-method ANY --authorization-type NONE --region ${config.region}`); + } catch (error) { + console.log('Method already exists, skipping...'); + } + + // Create integration + try { + execSync(`aws apigateway put-integration --rest-api-id ${apiId} --resource-id ${proxyResourceId} --http-method ANY --type AWS_PROXY --integration-http-method POST --uri arn:aws:apigateway:${config.region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${config.region}:${process.env.AWS_ACCOUNT_ID}:function:${config.functionName}/invocations --region ${config.region}`); + } catch (error) { + console.log('Integration already exists, skipping...'); + } + + // Deploy API + execSync(`aws apigateway create-deployment --rest-api-id ${apiId} --stage-name ${config.stageName} --region ${config.region}`); + + console.log(`API Gateway deployed: https://${apiId}.execute-api.${config.region}.amazonaws.com/${config.stageName}`); + + } catch (error) { + console.error('Error creating API Gateway:', error); + process.exit(1); + } +} + +// Clean up +function cleanup() { + console.log('Cleaning up...'); + + try { + execSync('rm -rf deploy'); + execSync('rm -f deployment.zip'); + + console.log('Cleanup completed'); + } catch (error) { + console.error('Error during cleanup:', error); + } +} + +// Main function +async function main() { + try { + console.log('Starting AWS deployment...'); + + createDeploymentPackage(); + deployToLambda(); + createApiGateway(); + cleanup(); + + console.log('Deployment completed successfully'); + } catch (error) { + console.error('Deployment failed:', error); + cleanup(); + process.exit(1); + } +} + +// Run the script +main(); diff --git a/cloud-wrapper/scripts/deploy-cloudflare.js b/cloud-wrapper/scripts/deploy-cloudflare.js new file mode 100644 index 00000000..be27f626 --- /dev/null +++ b/cloud-wrapper/scripts/deploy-cloudflare.js @@ -0,0 +1,245 @@ +#!/usr/bin/env node + +/** + * Cloudflare Deployment Script for Brainy Cloud Wrapper + * + * This script helps deploy the Brainy cloud wrapper to Cloudflare Workers. + * It uses the Wrangler CLI to create and configure the necessary resources. + * + * Prerequisites: + * - Wrangler CLI installed and configured with appropriate credentials + * - Node.js 23.11.0 or higher + * - Brainy cloud wrapper built (npm run build) + */ + +import { execSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import dotenv from 'dotenv'; + +// Load environment variables +dotenv.config(); + +// Configuration +const config = { + workerName: process.env.CF_WORKER_NAME || 'brainy-cloud-service', + accountId: process.env.CF_ACCOUNT_ID, + kvNamespace: process.env.CF_KV_NAMESPACE || 'BRAINY_STORAGE', + r2Bucket: process.env.CF_R2_BUCKET || 'brainy-storage' +}; + +// Validate configuration +if (!config.accountId) { + console.error('Error: CF_ACCOUNT_ID environment variable is required'); + process.exit(1); +} + +// Create wrangler.toml configuration +function createWranglerConfig() { + console.log('Creating wrangler.toml configuration...'); + + const wranglerContent = ` +name = "${config.workerName}" +main = "dist/worker.js" +compatibility_date = "2023-12-01" +node_compat = true + +account_id = "${config.accountId}" + +[build] +command = "npm run build" + +[vars] +NODE_ENV = "production" +STORAGE_TYPE = "${process.env.STORAGE_TYPE || 'memory'}" +USE_SIMPLE_EMBEDDING = "${process.env.USE_SIMPLE_EMBEDDING || 'false'}" + +# KV Namespace for storage +[[kv_namespaces]] +binding = "BRAINY_KV" +id = "${process.env.CF_KV_NAMESPACE_ID || 'create_kv_namespace_and_add_id_here'}" + +# R2 Bucket for storage (if using R2) +[[r2_buckets]] +binding = "BRAINY_R2" +bucket_name = "${config.r2Bucket}" +`; + + try { + fs.writeFileSync('wrangler.toml', wranglerContent); + console.log('wrangler.toml created successfully'); + } catch (error) { + console.error('Error creating wrangler.toml:', error); + process.exit(1); + } +} + +// Create Cloudflare Worker adapter +function createWorkerAdapter() { + console.log('Creating Cloudflare Worker adapter...'); + + const workerContent = ` +import { createServer } from '@cloudflare/workers-adapter'; +import app from './index.js'; + +// Create a fetch handler for the worker +export default { + async fetch(request, env, ctx) { + // Add environment variables to process.env + process.env = { + ...process.env, + ...env.vars, + CF_KV_NAMESPACE: env.BRAINY_KV, + CF_R2_BUCKET: env.BRAINY_R2 + }; + + // Create a server adapter + const server = createServer(app); + + // Handle the request + return server.fetch(request, env, ctx); + } +}; +`; + + try { + // Create dist directory if it doesn't exist + if (!fs.existsSync('dist')) { + fs.mkdirSync('dist'); + } + + fs.writeFileSync('dist/worker.js', workerContent); + console.log('Worker adapter created successfully'); + } catch (error) { + console.error('Error creating worker adapter:', error); + process.exit(1); + } +} + +// Update package.json to include Cloudflare Workers dependencies +function updatePackageJson() { + console.log('Updating package.json...'); + + try { + const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); + + // Add Cloudflare Workers dependencies + packageJson.dependencies = { + ...packageJson.dependencies, + '@cloudflare/workers-adapter': '^1.1.0', + '@cloudflare/kv-asset-handler': '^0.3.0' + }; + + // Add Cloudflare Workers scripts + packageJson.scripts = { + ...packageJson.scripts, + 'deploy:cloudflare': 'wrangler deploy' + }; + + fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2)); + console.log('package.json updated successfully'); + + // Install new dependencies + execSync('npm install --legacy-peer-deps'); + } catch (error) { + console.error('Error updating package.json:', error); + process.exit(1); + } +} + +// Create KV namespace if it doesn't exist +function createKVNamespace() { + console.log('Creating KV namespace...'); + + try { + // Check if KV namespace ID is provided + if (process.env.CF_KV_NAMESPACE_ID) { + console.log(`Using existing KV namespace: ${process.env.CF_KV_NAMESPACE_ID}`); + return; + } + + // Create KV namespace + const result = execSync(`wrangler kv:namespace create "${config.kvNamespace}"`).toString(); + const match = result.match(/id = "([^"]+)"/); + + if (match && match[1]) { + const namespaceId = match[1]; + console.log(`KV namespace created with ID: ${namespaceId}`); + + // Update wrangler.toml with the new namespace ID + let wranglerContent = fs.readFileSync('wrangler.toml', 'utf8'); + wranglerContent = wranglerContent.replace(/id = "create_kv_namespace_and_add_id_here"/, `id = "${namespaceId}"`); + fs.writeFileSync('wrangler.toml', wranglerContent); + } else { + console.error('Failed to extract KV namespace ID from wrangler output'); + } + } catch (error) { + console.error('Error creating KV namespace:', error); + console.log('You may need to create the KV namespace manually and update wrangler.toml'); + } +} + +// Create R2 bucket if it doesn't exist +function createR2Bucket() { + console.log('Creating R2 bucket...'); + + try { + // Only create R2 bucket if using R2 storage + if (process.env.STORAGE_TYPE !== 'r2') { + console.log('Skipping R2 bucket creation (not using R2 storage)'); + return; + } + + // Create R2 bucket + execSync(`wrangler r2 bucket create ${config.r2Bucket}`); + console.log(`R2 bucket created: ${config.r2Bucket}`); + } catch (error) { + console.error('Error creating R2 bucket:', error); + console.log('You may need to create the R2 bucket manually'); + } +} + +// Deploy to Cloudflare Workers +function deployToCloudflare() { + console.log('Deploying to Cloudflare Workers...'); + + try { + execSync('wrangler deploy', { stdio: 'inherit' }); + console.log('Deployed to Cloudflare Workers successfully'); + } catch (error) { + console.error('Error deploying to Cloudflare Workers:', error); + process.exit(1); + } +} + +// Clean up +function cleanup() { + console.log('Cleaning up...'); + + // No cleanup needed for now + console.log('Cleanup completed'); +} + +// Main function +async function main() { + try { + console.log('Starting Cloudflare deployment...'); + + createWranglerConfig(); + createWorkerAdapter(); + updatePackageJson(); + createKVNamespace(); + createR2Bucket(); + deployToCloudflare(); + cleanup(); + + console.log('Deployment completed successfully'); + } catch (error) { + console.error('Deployment failed:', error); + cleanup(); + process.exit(1); + } +} + +// Run the script +main(); diff --git a/cloud-wrapper/scripts/deploy-gcp.js b/cloud-wrapper/scripts/deploy-gcp.js new file mode 100644 index 00000000..8f388720 --- /dev/null +++ b/cloud-wrapper/scripts/deploy-gcp.js @@ -0,0 +1,196 @@ +#!/usr/bin/env node + +/** + * Google Cloud Platform Deployment Script for Brainy Cloud Wrapper + * + * This script helps deploy the Brainy cloud wrapper to Google Cloud Run. + * It uses the Google Cloud SDK to create and configure the necessary resources. + * + * Prerequisites: + * - Google Cloud SDK installed and configured with appropriate credentials + * - Node.js 23.11.0 or higher + * - Brainy cloud wrapper built (npm run build) + * - Docker installed (for building container images) + */ + +import { execSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import dotenv from 'dotenv'; + +// Load environment variables +dotenv.config(); + +// Configuration +const config = { + projectId: process.env.GCP_PROJECT_ID, + region: process.env.GCP_REGION || 'us-central1', + serviceName: process.env.GCP_SERVICE_NAME || 'brainy-cloud-service', + imageName: process.env.GCP_IMAGE_NAME || 'brainy-cloud-service', + memory: process.env.GCP_MEMORY || '512Mi', + cpu: process.env.GCP_CPU || '1', + maxInstances: process.env.GCP_MAX_INSTANCES || '10', + minInstances: process.env.GCP_MIN_INSTANCES || '0' +}; + +// Validate configuration +if (!config.projectId) { + console.error('Error: GCP_PROJECT_ID environment variable is required'); + process.exit(1); +} + +// Create Dockerfile +function createDockerfile() { + console.log('Creating Dockerfile...'); + + const dockerfileContent = ` +FROM node:23-slim + +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package*.json ./ + +# Install dependencies +RUN npm install --production --legacy-peer-deps + +# Copy built application +COPY dist/ ./dist/ + +# Copy environment variables +COPY .env ./ + +# Expose port +EXPOSE 8080 + +# Start the application +CMD ["node", "dist/index.js"] +`; + + try { + fs.writeFileSync('Dockerfile', dockerfileContent); + console.log('Dockerfile created successfully'); + } catch (error) { + console.error('Error creating Dockerfile:', error); + process.exit(1); + } +} + +// Build and push Docker image +function buildAndPushImage() { + console.log('Building and pushing Docker image...'); + + try { + // Set Google Cloud project + execSync(`gcloud config set project ${config.projectId}`); + + // Build the Docker image + execSync(`docker build -t gcr.io/${config.projectId}/${config.imageName} .`); + + // Configure Docker to use gcloud as a credential helper + execSync('gcloud auth configure-docker'); + + // Push the image to Google Container Registry + execSync(`docker push gcr.io/${config.projectId}/${config.imageName}`); + + console.log('Docker image built and pushed successfully'); + } catch (error) { + console.error('Error building and pushing Docker image:', error); + process.exit(1); + } +} + +// Deploy to Google Cloud Run +function deployToCloudRun() { + console.log('Deploying to Google Cloud Run...'); + + try { + // Create environment variables string + const envVars = [ + `NODE_ENV=production`, + `PORT=8080`, + `STORAGE_TYPE=${process.env.STORAGE_TYPE || 'filesystem'}` + ]; + + // Add S3 environment variables if using S3 storage + if (process.env.STORAGE_TYPE === 's3') { + envVars.push(`S3_BUCKET_NAME=${process.env.S3_BUCKET_NAME}`); + envVars.push(`S3_ACCESS_KEY_ID=${process.env.S3_ACCESS_KEY_ID}`); + envVars.push(`S3_SECRET_ACCESS_KEY=${process.env.S3_SECRET_ACCESS_KEY}`); + envVars.push(`S3_REGION=${process.env.S3_REGION || 'us-east-1'}`); + + if (process.env.S3_ENDPOINT) { + envVars.push(`S3_ENDPOINT=${process.env.S3_ENDPOINT}`); + } + } + + // Add embedding and HNSW configuration if provided + if (process.env.USE_SIMPLE_EMBEDDING) { + envVars.push(`USE_SIMPLE_EMBEDDING=${process.env.USE_SIMPLE_EMBEDDING}`); + } + + if (process.env.HNSW_M) { + envVars.push(`HNSW_M=${process.env.HNSW_M}`); + envVars.push(`HNSW_EF_CONSTRUCTION=${process.env.HNSW_EF_CONSTRUCTION || '200'}`); + envVars.push(`HNSW_EF_SEARCH=${process.env.HNSW_EF_SEARCH || '50'}`); + } + + // Deploy to Cloud Run + const envVarsString = envVars.map(env => `--set-env-vars="${env}"`).join(' '); + + execSync(`gcloud run deploy ${config.serviceName} \ + --image=gcr.io/${config.projectId}/${config.imageName} \ + --platform=managed \ + --region=${config.region} \ + --memory=${config.memory} \ + --cpu=${config.cpu} \ + --max-instances=${config.maxInstances} \ + --min-instances=${config.minInstances} \ + --allow-unauthenticated \ + ${envVarsString}`); + + console.log('Deployed to Google Cloud Run successfully'); + + // Get the service URL + const serviceUrl = execSync(`gcloud run services describe ${config.serviceName} --region=${config.region} --format="value(status.url)"`).toString().trim(); + console.log(`Service URL: ${serviceUrl}`); + } catch (error) { + console.error('Error deploying to Google Cloud Run:', error); + process.exit(1); + } +} + +// Clean up +function cleanup() { + console.log('Cleaning up...'); + + try { + // Remove Dockerfile + fs.unlinkSync('Dockerfile'); + + console.log('Cleanup completed'); + } catch (error) { + console.error('Error during cleanup:', error); + } +} + +// Main function +async function main() { + try { + console.log('Starting Google Cloud Platform deployment...'); + + createDockerfile(); + buildAndPushImage(); + deployToCloudRun(); + cleanup(); + + console.log('Deployment completed successfully'); + } catch (error) { + console.error('Deployment failed:', error); + cleanup(); + process.exit(1); + } +} + +// Run the script +main(); diff --git a/cloud-wrapper/src/index.ts b/cloud-wrapper/src/index.ts new file mode 100644 index 00000000..c4b4144a --- /dev/null +++ b/cloud-wrapper/src/index.ts @@ -0,0 +1,85 @@ +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'; +import { initializeBrainy } from './services/brainyService'; +import { setupWebSocketHandlers } from './websocket'; + +// 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 +app.use('/api', (req, res, next) => { + // Attach brainy instance to request + (req as any).brainy = brainyInstance; + next(); +}, setupRoutes()); + +// 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'); + + // 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(); diff --git a/cloud-wrapper/src/routes.ts b/cloud-wrapper/src/routes.ts new file mode 100644 index 00000000..ebdbfd36 --- /dev/null +++ b/cloud-wrapper/src/routes.ts @@ -0,0 +1,180 @@ +import { Router, Request, Response } 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; +} + +export function setupRoutes() { + const router = Router(); + + // Get database status + router.get('/status', async (req: BrainyRequest, res: Response) => { + 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', async (req: BrainyRequest, res: Response) => { + 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', async (req: BrainyRequest, res: Response) => { + 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', async (req: BrainyRequest, res: Response) => { + 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', async (req: BrainyRequest, res: Response) => { + 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', async (req: BrainyRequest, res: Response) => { + 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', async (req: BrainyRequest, res: Response) => { + 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', async (req: BrainyRequest, res: Response) => { + 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', async (req: BrainyRequest, res: Response) => { + 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', async (req: BrainyRequest, res: Response) => { + 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', async (req: BrainyRequest, res: Response) => { + 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', async (req: BrainyRequest, res: Response) => { + 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; +} diff --git a/cloud-wrapper/src/services/brainyService.ts b/cloud-wrapper/src/services/brainyService.ts new file mode 100644 index 00000000..9e6172ee --- /dev/null +++ b/cloud-wrapper/src/services/brainyService.ts @@ -0,0 +1,80 @@ +import { BrainyData } from '@soulcraft/brainy'; +import dotenv from 'dotenv'; + +// Load environment variables +dotenv.config(); + +/** + * Initialize the Brainy instance with appropriate configuration + * based on environment variables + */ +export async function initializeBrainy(): Promise { + // Get storage configuration from environment variables + const storageType = process.env.STORAGE_TYPE || 'filesystem'; + + // Create configuration object + const config: any = { + storage: {} + }; + + // Configure storage based on type + switch (storageType) { + case 's3': + // Configure S3 storage + 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', + endpoint: process.env.S3_ENDPOINT // Optional for custom S3-compatible services + }; + 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; + } + + // Configure embedding options + if (process.env.USE_SIMPLE_EMBEDDING === 'true') { + // Use simple embedding (faster but less accurate) + config.useSimpleEmbedding = true; + } + + // 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 { + const status = await brainy.status(); + return status.storageType || 'unknown'; +} diff --git a/cloud-wrapper/src/websocket.ts b/cloud-wrapper/src/websocket.ts new file mode 100644 index 00000000..5e6b5714 --- /dev/null +++ b/cloud-wrapper/src/websocket.ts @@ -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; +} + +// Store connected clients +const clients: Map = 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 + } + }); + } + } +} diff --git a/src/brainyData.ts b/src/brainyData.ts index 246f0257..93c2da02 100644 --- a/src/brainyData.ts +++ b/src/brainyData.ts @@ -115,9 +115,9 @@ export class BrainyData { this.embeddingFunction = config.embeddingFunction || defaultEmbeddingFunction // Set persistent storage request flag (support both new and deprecated options) - this.requestPersistentStorage = - (config.storage?.requestPersistentStorage !== undefined) - ? config.storage.requestPersistentStorage + this.requestPersistentStorage = + (config.storage?.requestPersistentStorage !== undefined) + ? config.storage.requestPersistentStorage : (config.requestPersistentStorage || false) // Set read-only flag @@ -153,9 +153,9 @@ export class BrainyData { const storageOptions = { ...this.storageConfig, requestPersistentStorage: this.requestPersistentStorage - }; + } - this.storage = await createStorage(storageOptions); + this.storage = await createStorage(storageOptions) } // Initialize storage @@ -246,15 +246,15 @@ export class BrainyData { if (metadata !== undefined) { // Validate noun type if metadata is for a GraphNoun if (metadata && typeof metadata === 'object' && 'noun' in metadata) { - const nounType = (metadata as any).noun; + const nounType = (metadata as any).noun // Check if the noun type is valid - const isValidNounType = Object.values(NounType).includes(nounType); + const isValidNounType = Object.values(NounType).includes(nounType) if (!isValidNounType) { console.warn(`Invalid noun type: ${nounType}. Falling back to GraphNoun.`); // Set a default noun type - (metadata as any).noun = NounType.Concept; + (metadata as any).noun = NounType.Concept } } @@ -439,23 +439,23 @@ export class BrainyData { } = {} ): Promise[]> { // If input is a string and not a vector, automatically vectorize it - let queryToUse = queryVectorOrData; + let queryToUse = queryVectorOrData if (typeof queryVectorOrData === 'string' && !options.forceEmbed) { - queryToUse = await this.embed(queryVectorOrData); - options.forceEmbed = false; // Already embedded, don't force again + queryToUse = await this.embed(queryVectorOrData) + options.forceEmbed = false // Already embedded, don't force again } // If noun types are specified, use searchByNounTypes - let searchResults; + let searchResults if (options.nounTypes && options.nounTypes.length > 0) { searchResults = await this.searchByNounTypes(queryToUse, k, options.nounTypes, { forceEmbed: options.forceEmbed - }); + }) } else { // Otherwise, search all GraphNouns searchResults = await this.searchByNounTypes(queryToUse, k, null, { forceEmbed: options.forceEmbed - }); + }) } // If includeVerbs is true, retrieve associated GraphVerbs for each result @@ -463,28 +463,28 @@ export class BrainyData { for (const result of searchResults) { try { // Get outgoing verbs for this noun - const outgoingVerbs = await this.storage.getVerbsBySource(result.id); + const outgoingVerbs = await this.storage.getVerbsBySource(result.id) // Get incoming verbs for this noun - const incomingVerbs = await this.storage.getVerbsByTarget(result.id); + const incomingVerbs = await this.storage.getVerbsByTarget(result.id) // Combine all verbs - const allVerbs = [...outgoingVerbs, ...incomingVerbs]; + const allVerbs = [...outgoingVerbs, ...incomingVerbs] // Add verbs to the result metadata if (!result.metadata) { - result.metadata = {} as T; + result.metadata = {} as T } // Add the verbs to the metadata - (result.metadata as any).associatedVerbs = allVerbs; + (result.metadata as any).associatedVerbs = allVerbs } catch (error) { - console.warn(`Failed to retrieve verbs for noun ${result.id}:`, error); + console.warn(`Failed to retrieve verbs for noun ${result.id}:`, error) } } } - return searchResults; + return searchResults } /** @@ -565,15 +565,15 @@ export class BrainyData { // Validate noun type if metadata is for a GraphNoun if (metadata && typeof metadata === 'object' && 'noun' in metadata) { - const nounType = (metadata as any).noun; + const nounType = (metadata as any).noun // Check if the noun type is valid - const isValidNounType = Object.values(NounType).includes(nounType); + const isValidNounType = Object.values(NounType).includes(nounType) if (!isValidNounType) { console.warn(`Invalid noun type: ${nounType}. Falling back to GraphNoun.`); // Set a default noun type - (metadata as any).noun = NounType.Concept; + (metadata as any).noun = NounType.Concept } } @@ -629,19 +629,19 @@ export class BrainyData { if (options.metadata && (!vector || options.forceEmbed)) { try { // Extract a string representation from metadata for embedding - let textToEmbed: string; + let textToEmbed: string if (typeof options.metadata === 'string') { - textToEmbed = options.metadata; + textToEmbed = options.metadata } else if (options.metadata.description && typeof options.metadata.description === 'string') { - textToEmbed = options.metadata.description; + textToEmbed = options.metadata.description } else { // Convert to JSON string as fallback - textToEmbed = JSON.stringify(options.metadata); + textToEmbed = JSON.stringify(options.metadata) } // Ensure textToEmbed is a string if (typeof textToEmbed !== 'string') { - textToEmbed = String(textToEmbed); + textToEmbed = String(textToEmbed) } verbVector = await this.embeddingFunction(textToEmbed) @@ -656,15 +656,15 @@ export class BrainyData { } // Validate verb type if provided - let verbType = options.type; + let verbType = options.type if (verbType) { // Check if the verb type is valid - const isValidVerbType = Object.values(VerbType).includes(verbType as any); + const isValidVerbType = Object.values(VerbType).includes(verbType as any) if (!isValidVerbType) { - console.warn(`Invalid verb type: ${verbType}. Using RelatedTo as default.`); + console.warn(`Invalid verb type: ${verbType}. Using RelatedTo as default.`) // Set a default verb type - verbType = VerbType.RelatedTo; + verbType = VerbType.RelatedTo } } @@ -873,8 +873,8 @@ export class BrainyData { * @returns Array of search results */ public async searchText( - query: string, - k: number = 10, + query: string, + k: number = 10, options: { nounTypes?: string[], includeVerbs?: boolean @@ -997,44 +997,44 @@ export class BrainyData { nounIds: string[]; verbIds: string[]; }> { - await this.ensureInitialized(); + await this.ensureInitialized() // Check if database is in read-only mode - this.checkReadOnly(); + this.checkReadOnly() // Set default options - const nounCount = options.nounCount || 10; - const verbCount = options.verbCount || 20; - const nounTypes = options.nounTypes || Object.values(NounType); - const verbTypes = options.verbTypes || Object.values(VerbType); - const clearExisting = options.clearExisting || false; + const nounCount = options.nounCount || 10 + const verbCount = options.verbCount || 20 + const nounTypes = options.nounTypes || Object.values(NounType) + const verbTypes = options.verbTypes || Object.values(VerbType) + const clearExisting = options.clearExisting || false // Clear existing data if requested if (clearExisting) { - await this.clear(); + await this.clear() } try { // Generate random nouns - const nounIds: string[] = []; + const nounIds: string[] = [] const nounDescriptions: Record = { - [NounType.Person]: "A person with unique characteristics", - [NounType.Place]: "A location with specific attributes", - [NounType.Thing]: "An object with distinct properties", - [NounType.Event]: "An occurrence with temporal aspects", - [NounType.Concept]: "An abstract idea or notion", - [NounType.Content]: "A piece of content or information", - [NounType.Group]: "A collection of related entities", - [NounType.List]: "An ordered sequence of items", - [NounType.Category]: "A classification or grouping" - }; + [NounType.Person]: 'A person with unique characteristics', + [NounType.Place]: 'A location with specific attributes', + [NounType.Thing]: 'An object with distinct properties', + [NounType.Event]: 'An occurrence with temporal aspects', + [NounType.Concept]: 'An abstract idea or notion', + [NounType.Content]: 'A piece of content or information', + [NounType.Group]: 'A collection of related entities', + [NounType.List]: 'An ordered sequence of items', + [NounType.Category]: 'A classification or grouping' + } for (let i = 0; i < nounCount; i++) { // Select a random noun type - const nounType = nounTypes[Math.floor(Math.random() * nounTypes.length)]; + const nounType = nounTypes[Math.floor(Math.random() * nounTypes.length)] // Generate a random label - const label = `Random ${nounType} ${i + 1}`; + const label = `Random ${nounType} ${i + 1}` // Create metadata const metadata = { @@ -1046,45 +1046,45 @@ export class BrainyData { priority: Math.floor(Math.random() * 5) + 1, tags: [`tag-${i % 5}`, `category-${i % 3}`] } - }; + } // Add the noun - const id = await this.add(metadata.description, metadata as T); - nounIds.push(id); + const id = await this.add(metadata.description, metadata as T) + nounIds.push(id) } // Generate random verbs between nouns - const verbIds: string[] = []; + const verbIds: string[] = [] const verbDescriptions: Record = { - [VerbType.AttributedTo]: "Attribution relationship", - [VerbType.Controls]: "Control relationship", - [VerbType.Created]: "Creation relationship", - [VerbType.Earned]: "Achievement relationship", - [VerbType.Owns]: "Ownership relationship", - [VerbType.MemberOf]: "Membership relationship", - [VerbType.RelatedTo]: "General relationship", - [VerbType.WorksWith]: "Collaboration relationship", - [VerbType.FriendOf]: "Friendship relationship", - [VerbType.ReportsTo]: "Reporting relationship", - [VerbType.Supervises]: "Supervision relationship", - [VerbType.Mentors]: "Mentorship relationship" - }; + [VerbType.AttributedTo]: 'Attribution relationship', + [VerbType.Controls]: 'Control relationship', + [VerbType.Created]: 'Creation relationship', + [VerbType.Earned]: 'Achievement relationship', + [VerbType.Owns]: 'Ownership relationship', + [VerbType.MemberOf]: 'Membership relationship', + [VerbType.RelatedTo]: 'General relationship', + [VerbType.WorksWith]: 'Collaboration relationship', + [VerbType.FriendOf]: 'Friendship relationship', + [VerbType.ReportsTo]: 'Reporting relationship', + [VerbType.Supervises]: 'Supervision relationship', + [VerbType.Mentors]: 'Mentorship relationship' + } for (let i = 0; i < verbCount; i++) { // Select random source and target nouns - const sourceIndex = Math.floor(Math.random() * nounIds.length); - let targetIndex = Math.floor(Math.random() * nounIds.length); + const sourceIndex = Math.floor(Math.random() * nounIds.length) + let targetIndex = Math.floor(Math.random() * nounIds.length) // Ensure source and target are different while (targetIndex === sourceIndex && nounIds.length > 1) { - targetIndex = Math.floor(Math.random() * nounIds.length); + targetIndex = Math.floor(Math.random() * nounIds.length) } - const sourceId = nounIds[sourceIndex]; - const targetId = nounIds[targetIndex]; + const sourceId = nounIds[sourceIndex] + const targetId = nounIds[targetIndex] // Select a random verb type - const verbType = verbTypes[Math.floor(Math.random() * verbTypes.length)]; + const verbType = verbTypes[Math.floor(Math.random() * verbTypes.length)] // Create metadata const metadata = { @@ -1097,25 +1097,25 @@ export class BrainyData { duration: Math.floor(Math.random() * 365) + 1, tags: [`relation-${i % 5}`, `strength-${i % 3}`] } - }; + } // Add the verb const id = await this.addVerb(sourceId, targetId, undefined, { type: verbType, weight: metadata.weight, metadata - }); + }) - verbIds.push(id); + verbIds.push(id) } return { nounIds, verbIds - }; + } } catch (error) { - console.error('Failed to generate random graph:', error); - throw new Error(`Failed to generate random graph: ${error}`); + console.error('Failed to generate random graph:', error) + throw new Error(`Failed to generate random graph: ${error}`) } } }