Initial commit

This commit is contained in:
David Snelling 2025-06-24 11:41:30 -07:00
commit 5a8a6c1ba3
81 changed files with 39269 additions and 0 deletions

View file

@ -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

341
cloud-wrapper/README.md Normal file
View file

@ -0,0 +1,341 @@
<div align="center">
<img src="../brainy.png" alt="Brainy Logo" width="200"/>
# Brainy Cloud Wrapper
</div>
A standalone web service wrapper for the [Brainy](https://github.com/soulcraft/brainy) vector graph database. This wrapper allows you to deploy Brainy as a RESTful API service on various cloud platforms including AWS, Google Cloud, and Cloudflare.
## Features
- RESTful API for all Brainy operations
- WebSocket API for real-time updates and subscriptions
- Model Control Protocol (MCP) service for external model access
- Support for multiple storage backends (Memory, FileSystem, S3)
- Configurable via environment variables
- Deployment scripts for AWS, Google Cloud, and Cloudflare
- Secure by default with Helmet middleware
- Cross-origin resource sharing (CORS) support
## Prerequisites
- Node.js 23.11.0 or higher
- npm or yarn
- For cloud deployments:
- AWS: AWS CLI installed and configured
- Google Cloud: Google Cloud SDK installed and configured
- Cloudflare: Wrangler CLI installed and configured
## Installation
1. Clone the repository:
```bash
git clone https://github.com/soulcraft/brainy.git
cd brainy/cloud-wrapper
```
2. Install dependencies:
```bash
npm install --legacy-peer-deps
```
3. Create a `.env` file based on the example:
```bash
cp .env.example .env
```
4. Edit the `.env` file to configure your environment.
## Configuration
The cloud wrapper can be configured using environment variables. See the `.env.example` file for available options.
### Storage Options
- `STORAGE_TYPE`: The type of storage to use. Options:
- `memory`: In-memory storage (default for Cloudflare)
- `filesystem`: File system storage (default for local and AWS/GCP)
- `s3`: S3-compatible storage (AWS S3, MinIO, etc.)
- `r2`: Cloudflare R2 storage (Cloudflare only)
### S3 Storage Configuration
When using `STORAGE_TYPE=s3`, the following environment variables are required:
- `S3_BUCKET_NAME`: The name of the S3 bucket
- `S3_ACCESS_KEY_ID`: Your S3 access key ID
- `S3_SECRET_ACCESS_KEY`: Your S3 secret access key
- `S3_REGION`: The S3 region (default: `us-east-1`)
- `S3_ENDPOINT` (optional): Custom endpoint for S3-compatible services
### MCP Service Configuration
The Model Control Protocol (MCP) service can be configured using the following environment variables:
- `MCP_WS_PORT`: Port for the MCP WebSocket server (if not set, WebSocket server is disabled)
- `MCP_REST_PORT`: Port for the MCP REST server (if not set, REST server is disabled)
- `MCP_ENABLE_AUTH`: Enable authentication for MCP requests (`true` or `false`)
- `MCP_API_KEYS`: Comma-separated list of API keys for authentication
- `MCP_RATE_LIMIT_REQUESTS`: Maximum number of requests per time window
- `MCP_RATE_LIMIT_WINDOW_MS`: Time window for rate limiting in milliseconds (default: `60000`)
- `MCP_ENABLE_CORS`: Enable CORS for MCP REST server (`true` or `false`)
## Local Development
1. Build the project:
```bash
npm run build
```
2. Start the development server:
```bash
npm run dev
```
3. The API will be available at `http://localhost:3000`.
## API Endpoints
Brainy Cloud Wrapper provides REST API, WebSocket API, and Model Control Protocol (MCP) API for interacting with the database.
### REST API
#### Status
- `GET /api/status`: Get database status
#### Nouns (Entities)
- `POST /api/nouns`: Add a new noun
- Body: `{ "text": "Your text", "metadata": { ... } }`
- `GET /api/nouns/:id`: Get a noun by ID
- `PUT /api/nouns/:id`: Update noun metadata
- Body: `{ "metadata": { ... } }`
- `DELETE /api/nouns/:id`: Delete a noun
#### Search
- `POST /api/search`: Search for similar nouns
- Body: `{ "query": "Your search query", "limit": 10 }`
#### Verbs (Relationships)
- `POST /api/verbs`: Add a relationship between nouns
- Body: `{ "sourceId": "...", "targetId": "...", "metadata": { ... } }`
- `GET /api/verbs`: Get all relationships
- `GET /api/verbs/source/:id`: Get relationships by source
- `GET /api/verbs/target/:id`: Get relationships by target
- `DELETE /api/verbs/:id`: Delete a relationship
#### Database Management
- `DELETE /api/clear`: Clear all data
### WebSocket API
The WebSocket API provides real-time communication with the Brainy database. Connect to the WebSocket server at `ws://your-server:port`.
#### Message Format
All WebSocket messages follow this format:
```json
{
"type": "messageType",
"id": "unique-message-id",
"payload": {
// Message-specific data
}
}
```
#### Available Message Types
##### Status
- Request: `{ "type": "status" }`
- Response: `{ "type": "status", "id": "...", "payload": { /* database status */ } }`
##### Nouns (Entities)
- Add a noun:
- Request: `{ "type": "addNoun", "payload": { "text": "Your text", "metadata": { ... } } }`
- Response: `{ "type": "addNoun", "id": "...", "payload": { "id": "new-noun-id" } }`
- Get a noun:
- Request: `{ "type": "getNoun", "payload": { "id": "noun-id" } }`
- Response: `{ "type": "getNoun", "id": "...", "payload": { /* noun data */ } }`
- Update a noun:
- Request: `{ "type": "updateNoun", "payload": { "id": "noun-id", "metadata": { ... } } }`
- Response: `{ "type": "updateNoun", "id": "...", "payload": { "success": true } }`
- Delete a noun:
- Request: `{ "type": "deleteNoun", "payload": { "id": "noun-id" } }`
- Response: `{ "type": "deleteNoun", "id": "...", "payload": { "success": true } }`
##### Search
- Search for similar nouns:
- Request: `{ "type": "search", "payload": { "query": "Your search query", "limit": 10 } }`
- Response: `{ "type": "search", "id": "...", "payload": [ /* search results */ ] }`
##### Verbs (Relationships)
- Add a verb:
- Request: `{ "type": "addVerb", "payload": { "sourceId": "...", "targetId": "...", "metadata": { ... } } }`
- Response: `{ "type": "addVerb", "id": "...", "payload": { "success": true } }`
- Get all verbs:
- Request: `{ "type": "getVerbs" }`
- Response: `{ "type": "getVerbs", "id": "...", "payload": [ /* all verbs */ ] }`
- Get verbs by source:
- Request: `{ "type": "getVerbsBySource", "payload": { "id": "source-id" } }`
- Response: `{ "type": "getVerbsBySource", "id": "...", "payload": [ /* verbs */ ] }`
- Get verbs by target:
- Request: `{ "type": "getVerbsByTarget", "payload": { "id": "target-id" } }`
- Response: `{ "type": "getVerbsByTarget", "id": "...", "payload": [ /* verbs */ ] }`
- Delete a verb:
- Request: `{ "type": "deleteVerb", "payload": { "id": "verb-id" } }`
- Response: `{ "type": "deleteVerb", "id": "...", "payload": { "success": true } }`
##### Database Management
- Clear all data:
- Request: `{ "type": "clear" }`
- Response: `{ "type": "clear", "id": "...", "payload": { "success": true } }`
#### Real-time Subscriptions
The WebSocket API supports subscribing to real-time updates:
- Subscribe to updates:
- Request: `{ "type": "subscribe", "payload": { "type": "nouns" } }`
- Response: `{ "type": "subscribe", "id": "...", "payload": { "success": true, "type": "nouns" } }`
- Unsubscribe from updates:
- Request: `{ "type": "unsubscribe", "payload": { "type": "nouns" } }`
- Response: `{ "type": "unsubscribe", "id": "...", "payload": { "success": true, "type": "nouns" } }`
Available subscription types:
- `nouns`: Updates about nouns (added, updated, deleted)
- `verbs`: Updates about verbs (added, deleted)
- `searchResults`: Updates about search results
When subscribed, you'll receive messages when relevant events occur:
```json
{
"type": "subscribe",
"payload": {
"type": "nouns",
"data": {
"type": "added",
"id": "noun-id",
"data": { /* noun data */ }
}
}
}
```
### MCP API
The Model Control Protocol (MCP) API provides a standardized interface for external models to access Brainy data and use the augmentation pipeline as tools. The MCP API is available through both WebSocket and REST endpoints.
#### MCP REST API Endpoints
- `POST /mcp/data`: Access Brainy data (search, get, add, etc.)
- `POST /mcp/tools`: Execute augmentation pipeline tools
- `POST /mcp/system`: Get system information
- `POST /mcp/auth`: Authenticate with the MCP service
- `GET /mcp/tools`: Get available tools
#### MCP WebSocket
Connect to the MCP WebSocket server at `ws://your-server:MCP_WS_PORT` and send JSON messages in the MCP format:
```json
{
"type": "DATA_ACCESS",
"requestId": "unique-request-id",
"version": "1.0",
"operation": "search",
"parameters": {
"query": "Your search query",
"k": 5
}
}
```
For detailed documentation on the MCP API, see the [MCP documentation](../src/mcp/README.md).
## Cloud Deployment
### AWS Lambda and API Gateway
1. Configure AWS-specific environment variables:
```
AWS_REGION=us-east-1
AWS_FUNCTION_NAME=brainy-cloud-service
AWS_API_GATEWAY_NAME=brainy-api
AWS_STAGE_NAME=prod
AWS_ACCOUNT_ID=your-account-id
```
2. Deploy to AWS:
```bash
npm run deploy:aws
```
### Google Cloud Run
1. Configure GCP-specific environment variables:
```
GCP_PROJECT_ID=your-project-id
GCP_REGION=us-central1
GCP_SERVICE_NAME=brainy-cloud-service
GCP_IMAGE_NAME=brainy-cloud-service
GCP_MEMORY=512Mi
GCP_CPU=1
GCP_MAX_INSTANCES=10
GCP_MIN_INSTANCES=0
```
2. Deploy to Google Cloud:
```bash
npm run deploy:gcp
```
### Cloudflare Workers
1. Configure Cloudflare-specific environment variables:
```
CF_ACCOUNT_ID=your-account-id
CF_WORKER_NAME=brainy-cloud-service
CF_KV_NAMESPACE=BRAINY_STORAGE
CF_R2_BUCKET=brainy-storage
```
2. Deploy to Cloudflare:
```bash
npm run deploy:cloudflare
```
## Storage Considerations
### AWS Lambda
When deploying to AWS Lambda, it's recommended to use S3 storage for persistence. The filesystem storage option will work but data will be lost when the Lambda function is recycled.
### Google Cloud Run
For Google Cloud Run, you can use either filesystem storage (for ephemeral storage) or S3-compatible storage (like Google Cloud Storage with an S3 compatibility layer).
### Cloudflare Workers
Cloudflare Workers have limited storage options. The recommended approach is to use:
- Cloudflare KV for small datasets
- Cloudflare R2 for larger datasets
- Memory storage for temporary data
## License
MIT

4082
cloud-wrapper/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,92 @@
{
"name": "@soulcraft/brainy-cloud",
"version": "0.1.0",
"description": "Cloud deployment wrapper for Brainy vector graph database",
"main": "dist/index.js",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "nodemon --exec node --experimental-specifier-resolution=node --loader ts-node/esm src/index.ts",
"deploy:aws": "node scripts/deploy-aws.js",
"deploy:gcp": "node scripts/deploy-gcp.js",
"deploy:cloudflare": "node scripts/deploy-cloudflare.js"
},
"keywords": [
"brainy",
"vector-database",
"cloud",
"aws",
"google-cloud",
"cloudflare"
],
"author": "David Snelling (david@soulcraft.com)",
"license": "MIT",
"dependencies": {
"@soulcraft/brainy": "^0.9.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"helmet": "^7.1.0",
"morgan": "^1.10.0",
"uuid": "^9.0.1",
"ws": "^8.16.0"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/morgan": "^1.9.9",
"@types/node": "^20.10.5",
"@types/uuid": "^9.0.7",
"@types/ws": "^8.5.10",
"nodemon": "^3.0.2",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
},
"engines": {
"node": ">=23.11.0"
},
"prettier": {
"arrowParens": "always",
"bracketSameLine": true,
"bracketSpacing": true,
"htmlWhitespaceSensitivity": "css",
"printWidth": 80,
"proseWrap": "preserve",
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "none",
"useTabs": false
},
"eslintConfig": {
"root": true,
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"@typescript-eslint/no-explicit-any": "off",
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{
"args": "after-used",
"argsIgnorePattern": "^_"
}
],
"semi": [
"error",
"never"
],
"@typescript-eslint/semi": [
"error",
"never"
]
}
}
}

View file

@ -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();

View file

@ -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();

View file

@ -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();

106
cloud-wrapper/src/index.ts Normal file
View 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
View 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;
}

View 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'
}

View 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
}

View 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
}
});
}
}
}

View file

@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"declaration": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}