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