feat: add zero-configuration Brainy service template with augmentation-first architecture
- Implement WebSocket augmentation for real-time communication - Implement WebRTC augmentation for peer-to-peer connections - Implement HTTP augmentation as minimal REST fallback - Add auto-discovery augmentation for data pattern analysis - Add adaptive storage augmentation for intelligent resource management - Add environment adapter augmentation for universal compatibility - Template auto-detects environment (browser, Node.js, serverless, containers) - Intelligent transport selection (WebRTC → WebSocket → HTTP) - Automatic storage optimization (memory → filesystem → S3) - Zero configuration required - just npm start - Includes intelligent verb scoring by default - Works in any environment without configuration - Full documentation and examples included
This commit is contained in:
parent
880b8f74e3
commit
ee003a9473
30 changed files with 3548 additions and 0 deletions
149
examples/brainy-service-template/.gitignore
vendored
Normal file
149
examples/brainy-service-template/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
# Dependencies
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
.env.production
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
public
|
||||
|
||||
# Storybook build outputs
|
||||
.out
|
||||
.storybook-out
|
||||
|
||||
# Temporary folders
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Dependency directory
|
||||
node_modules
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Brainy data directories
|
||||
data/
|
||||
*.brainy
|
||||
*.db
|
||||
|
||||
# Docker
|
||||
.dockerignore
|
||||
|
||||
# Test artifacts
|
||||
test-results/
|
||||
coverage/
|
||||
|
||||
# Build outputs
|
||||
build/
|
||||
dist/
|
||||
44
examples/brainy-service-template/Dockerfile
Normal file
44
examples/brainy-service-template/Dockerfile
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
FROM node:24-alpine
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apk add --no-cache \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
git
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci --only=production
|
||||
|
||||
# Download Brainy models for offline operation
|
||||
RUN npm run download-models
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Create logs directory
|
||||
RUN mkdir -p logs
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup -g 1001 -S nodejs && \
|
||||
adduser -S brainy -u 1001 -G nodejs
|
||||
|
||||
# Change ownership
|
||||
RUN chown -R brainy:nodejs /app
|
||||
USER brainy
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||
CMD node -e "fetch('http://localhost:3000/health/liveness').then(r=>r.ok?process.exit(0):process.exit(1)).catch(()=>process.exit(1))"
|
||||
|
||||
# Start the application
|
||||
CMD ["npm", "start"]
|
||||
407
examples/brainy-service-template/README.md
Normal file
407
examples/brainy-service-template/README.md
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
# Brainy Service Template
|
||||
|
||||
**Zero-configuration, intelligent service template built with Brainy's native augmentation system.**
|
||||
|
||||
This template embodies Brainy's core tenets: **zero configuration**, **intelligent adaptation**, and **augmentation-first architecture**. It automatically adapts to any environment (browser, Node.js, serverless, containers) and uses WebRTC, WebSocket, and HTTP transport layers as needed.
|
||||
|
||||
## Features
|
||||
|
||||
🧠 **Zero Configuration**
|
||||
- Auto-detects optimal storage (memory → filesystem → S3)
|
||||
- Intelligent transport selection (WebRTC → WebSocket → HTTP)
|
||||
- Environment adaptation (browser, Node.js, serverless, containers)
|
||||
- Self-optimizing performance and caching
|
||||
|
||||
🔌 **Augmentation-First Architecture**
|
||||
- WebSocket Augmentation - Real-time queries and updates
|
||||
- WebRTC Augmentation - Peer-to-peer Brainy connections
|
||||
- HTTP Augmentation - Minimal REST API for universal access
|
||||
- Auto-Discovery Augmentation - Understand your data patterns
|
||||
- Adaptive Storage Augmentation - Intelligent resource management
|
||||
- Environment Adapter - Works everywhere automatically
|
||||
|
||||
🌐 **Universal Compatibility**
|
||||
- Browser (OPFS, IndexedDB, WebRTC peer-to-peer)
|
||||
- Node.js (filesystem, worker threads, clustering)
|
||||
- Serverless (memory, fast cold starts)
|
||||
- Docker/Containers (volumes, health checks)
|
||||
- Kubernetes (pod-aware, service mesh ready)
|
||||
- Edge computing (ultra-low latency, minimal footprint)
|
||||
|
||||
⚡ **Intelligent Features**
|
||||
- Intelligent Verb Scoring (enabled by default)
|
||||
- Automatic relationship weighting and confidence scoring
|
||||
- Learning from usage patterns and feedback
|
||||
- Real-time performance optimization
|
||||
- Data quality analysis and recommendations
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Zero-Config Instant Start
|
||||
|
||||
```bash
|
||||
# Copy template
|
||||
cp -r examples/brainy-service-template my-brainy-service
|
||||
cd my-brainy-service
|
||||
npm install
|
||||
|
||||
# Start with ZERO configuration - everything auto-detected!
|
||||
npm start
|
||||
```
|
||||
|
||||
**That's it!** The service automatically:
|
||||
- ✅ Detects your environment (Node.js, browser, container, etc.)
|
||||
- ✅ Chooses optimal storage (memory → filesystem → S3)
|
||||
- ✅ Enables best transport layers (WebRTC → WebSocket → HTTP)
|
||||
- ✅ Configures intelligent verb scoring
|
||||
- ✅ Sets up real-time capabilities
|
||||
- ✅ Optimizes performance for your hardware
|
||||
|
||||
### 2. Access Your Brainy Service
|
||||
|
||||
The service automatically provides multiple ways to interact:
|
||||
|
||||
```javascript
|
||||
// WebSocket (real-time, best performance)
|
||||
const ws = new WebSocket('ws://localhost:3001')
|
||||
ws.send(JSON.stringify({
|
||||
type: 'search',
|
||||
payload: { query: 'machine learning', limit: 10 }
|
||||
}))
|
||||
|
||||
// WebRTC (peer-to-peer, direct connection)
|
||||
// Client code automatically generated at http://localhost:3002
|
||||
|
||||
// HTTP (universal fallback)
|
||||
curl -X POST http://localhost:3000/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "machine learning", "limit": 10}'
|
||||
```
|
||||
|
||||
### 3. Optional: Environment Variables
|
||||
|
||||
Only set these if you want to override the intelligent defaults:
|
||||
|
||||
```bash
|
||||
# Storage preference (auto-detected by default)
|
||||
export BRAINY_STORAGE_TYPE=s3
|
||||
export BRAINY_S3_BUCKET=my-brainy-data
|
||||
|
||||
# Transport preference (all enabled by default)
|
||||
export BRAINY_TRANSPORTS=websocket,http # disable WebRTC
|
||||
|
||||
# Port preference (3000 by default)
|
||||
export PORT=8080
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Entities
|
||||
- `POST /api/entities` - Create entity
|
||||
- `GET /api/entities/:id` - Get entity by ID
|
||||
- `PUT /api/entities/:id` - Update entity
|
||||
- `DELETE /api/entities/:id` - Delete entity
|
||||
- `GET /api/entities` - List entities (paginated)
|
||||
- `POST /api/entities/search` - Search entities
|
||||
|
||||
### Relationships
|
||||
- `POST /api/relationships` - Create relationship
|
||||
- `GET /api/relationships/:id` - Get relationship by ID
|
||||
- `PUT /api/relationships/:id` - Update relationship
|
||||
- `DELETE /api/relationships/:id` - Delete relationship
|
||||
- `GET /api/relationships` - List relationships (paginated)
|
||||
|
||||
### Intelligent Scoring (when enabled)
|
||||
- `POST /api/scoring/feedback/:id` - Provide feedback for learning
|
||||
- `GET /api/scoring/stats` - Get scoring statistics
|
||||
- `POST /api/scoring/export` - Export learning data
|
||||
- `POST /api/scoring/import` - Import learning data
|
||||
- `DELETE /api/scoring/stats` - Clear statistics
|
||||
- `POST /api/scoring/analyze` - Analyze relationship patterns
|
||||
- `GET /api/scoring/recommendations/:entityId` - Get recommendations
|
||||
|
||||
### Health & Monitoring
|
||||
- `GET /health` - Overall health check
|
||||
- `GET /health/liveness` - Kubernetes liveness probe
|
||||
- `GET /health/readiness` - Kubernetes readiness probe
|
||||
- `GET /health/metrics` - System metrics
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"service": {
|
||||
"name": "my-service",
|
||||
"port": 3000,
|
||||
"cors": {
|
||||
"enabled": true,
|
||||
"origins": ["http://localhost:3000"]
|
||||
}
|
||||
},
|
||||
"brainy": {
|
||||
"storage": {
|
||||
"type": "filesystem|memory|s3",
|
||||
"path": "./data",
|
||||
"s3": {
|
||||
"bucket": "my-bucket",
|
||||
"region": "us-east-1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Features
|
||||
|
||||
```json
|
||||
{
|
||||
"brainy": {
|
||||
"features": {
|
||||
"intelligentVerbScoring": true,
|
||||
"realTimeUpdates": true,
|
||||
"distributedMode": false
|
||||
},
|
||||
"intelligentVerbScoring": {
|
||||
"enabled": true,
|
||||
"enableSemanticScoring": true,
|
||||
"enableFrequencyAmplification": true,
|
||||
"enableTemporalDecay": true,
|
||||
"temporalDecayRate": 0.01,
|
||||
"minWeight": 0.1,
|
||||
"maxWeight": 1.0,
|
||||
"baseConfidence": 0.5,
|
||||
"learningRate": 0.1
|
||||
},
|
||||
"cache": {
|
||||
"autoTune": true,
|
||||
"hotCacheMaxSize": 10000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Production (Local)
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm start
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
docker build -t my-brainy-service .
|
||||
docker run -p 3000:3000 my-brainy-service
|
||||
```
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```bash
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `NODE_ENV` | Environment | `development` |
|
||||
| `PORT` | Server port | `3000` |
|
||||
| `BRAINY_STORAGE_TYPE` | Storage type | `filesystem` |
|
||||
| `BRAINY_STORAGE_PATH` | Storage path | `./data` |
|
||||
| `BRAINY_INTELLIGENT_SCORING` | Enable scoring | `false` |
|
||||
| `LOG_LEVEL` | Logging level | `info` |
|
||||
|
||||
## Examples
|
||||
|
||||
### Adding Entities with Smart Relationships
|
||||
|
||||
```javascript
|
||||
// Add entities
|
||||
const person = await fetch('/entities', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
data: 'John is a senior software developer with expertise in React and Node.js',
|
||||
metadata: { type: 'person', name: 'John' }
|
||||
})
|
||||
})
|
||||
|
||||
const project = await fetch('/entities', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
data: 'E-commerce platform built with React and Node.js',
|
||||
metadata: { type: 'project', name: 'ShopApp' }
|
||||
})
|
||||
})
|
||||
|
||||
// Add relationship (with intelligent scoring if enabled)
|
||||
const relationship = await fetch('/relationships', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sourceId: person.id,
|
||||
targetId: project.id,
|
||||
type: 'contributesTo',
|
||||
// weight is automatically computed if intelligent scoring is enabled
|
||||
metadata: { role: 'lead developer' }
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Providing Learning Feedback
|
||||
|
||||
```javascript
|
||||
// Correct a relationship weight (helps the system learn)
|
||||
await fetch(`/relationships/${relationshipId}/feedback`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
weight: 0.9, // corrected weight
|
||||
confidence: 0.85, // corrected confidence
|
||||
type: 'correction'
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Semantic Search
|
||||
|
||||
```javascript
|
||||
const results = await fetch('/entities/search', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
query: 'experienced React developer',
|
||||
limit: 10,
|
||||
threshold: 0.7
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
brainy-service-template/
|
||||
├── src/
|
||||
│ ├── controllers/
|
||||
│ │ ├── entities.js
|
||||
│ │ ├── relationships.js
|
||||
│ │ └── scoring.js
|
||||
│ ├── middleware/
|
||||
│ │ ├── auth.js
|
||||
│ │ ├── validation.js
|
||||
│ │ └── errorHandler.js
|
||||
│ ├── services/
|
||||
│ │ ├── brainyService.js
|
||||
│ │ └── scoringService.js
|
||||
│ ├── utils/
|
||||
│ │ ├── config.js
|
||||
│ │ ├── logger.js
|
||||
│ │ └── helpers.js
|
||||
│ └── app.js
|
||||
├── config/
|
||||
│ ├── default.json
|
||||
│ ├── development.json
|
||||
│ └── production.json
|
||||
├── docker/
|
||||
│ ├── Dockerfile
|
||||
│ └── docker-compose.yml
|
||||
├── tests/
|
||||
│ ├── integration/
|
||||
│ └── unit/
|
||||
└── docs/
|
||||
└── api.md
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
npm test
|
||||
|
||||
# Run with coverage
|
||||
npm run test:coverage
|
||||
|
||||
# Run integration tests
|
||||
npm run test:integration
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
### Adding Custom Endpoints
|
||||
|
||||
```javascript
|
||||
// src/controllers/custom.js
|
||||
export const customEndpoint = async (req, res) => {
|
||||
const { brainyService } = req.app.locals
|
||||
|
||||
// Your custom logic using Brainy
|
||||
const results = await brainyService.search(req.body.query)
|
||||
|
||||
res.json({ results })
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Augmentations
|
||||
|
||||
```javascript
|
||||
// src/services/customAugmentation.js
|
||||
import { ICognitionAugmentation } from '@soulcraft/brainy'
|
||||
|
||||
export class CustomAugmentation implements ICognitionAugmentation {
|
||||
// Your custom augmentation logic
|
||||
}
|
||||
```
|
||||
|
||||
## Monitoring & Observability
|
||||
|
||||
The template includes built-in monitoring:
|
||||
|
||||
- **Health checks**: `/health` endpoint
|
||||
- **Metrics**: Request counts, response times, error rates
|
||||
- **Logging**: Structured JSON logging with correlation IDs
|
||||
- **Performance**: Automatic performance tracking for Brainy operations
|
||||
|
||||
## Production Considerations
|
||||
|
||||
### Security
|
||||
|
||||
- Input validation and sanitization
|
||||
- Rate limiting
|
||||
- CORS configuration
|
||||
- Environment-based secrets management
|
||||
|
||||
### Performance
|
||||
|
||||
- Connection pooling
|
||||
- Request caching
|
||||
- Automatic cache tuning
|
||||
- Background processing for heavy operations
|
||||
|
||||
### Reliability
|
||||
|
||||
- Graceful shutdown handling
|
||||
- Circuit breaker patterns
|
||||
- Retry logic with exponential backoff
|
||||
- Health checks for dependencies
|
||||
|
||||
## Support & Documentation
|
||||
|
||||
- [Brainy Documentation](../../docs/)
|
||||
- [Intelligent Verb Scoring Guide](../../docs/guides/intelligent-verb-scoring.md)
|
||||
- [API Reference](./docs/api.md)
|
||||
- [Configuration Examples](./config/)
|
||||
|
||||
## License
|
||||
|
||||
This template is provided under the same license as Brainy.
|
||||
52
examples/brainy-service-template/config/default.json
Normal file
52
examples/brainy-service-template/config/default.json
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"service": {
|
||||
"name": "brainy-service-template",
|
||||
"port": 3000,
|
||||
"cors": {
|
||||
"enabled": true,
|
||||
"origins": ["http://localhost:3000", "http://localhost:3001"]
|
||||
},
|
||||
"rateLimit": {
|
||||
"windowMs": 900000,
|
||||
"maxRequests": 100
|
||||
}
|
||||
},
|
||||
"brainy": {
|
||||
"storage": {
|
||||
"type": "filesystem",
|
||||
"path": "./data"
|
||||
},
|
||||
"features": {
|
||||
"intelligentVerbScoring": false,
|
||||
"realTimeUpdates": false,
|
||||
"distributedMode": false,
|
||||
"metadataIndexing": true
|
||||
},
|
||||
"intelligentVerbScoring": {
|
||||
"enabled": false,
|
||||
"enableSemanticScoring": true,
|
||||
"enableFrequencyAmplification": true,
|
||||
"enableTemporalDecay": true,
|
||||
"temporalDecayRate": 0.01,
|
||||
"minWeight": 0.1,
|
||||
"maxWeight": 1.0,
|
||||
"baseConfidence": 0.5,
|
||||
"learningRate": 0.1
|
||||
},
|
||||
"cache": {
|
||||
"autoTune": true,
|
||||
"hotCacheMaxSize": 5000
|
||||
},
|
||||
"logging": {
|
||||
"verbose": false
|
||||
}
|
||||
},
|
||||
"logging": {
|
||||
"level": "info",
|
||||
"format": "json",
|
||||
"timestamp": true
|
||||
},
|
||||
"health": {
|
||||
"timeout": 5000
|
||||
}
|
||||
}
|
||||
20
examples/brainy-service-template/config/development.json
Normal file
20
examples/brainy-service-template/config/development.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"service": {
|
||||
"port": 3000
|
||||
},
|
||||
"brainy": {
|
||||
"features": {
|
||||
"intelligentVerbScoring": true
|
||||
},
|
||||
"intelligentVerbScoring": {
|
||||
"enabled": true,
|
||||
"baseConfidence": 0.6
|
||||
},
|
||||
"logging": {
|
||||
"verbose": true
|
||||
}
|
||||
},
|
||||
"logging": {
|
||||
"level": "debug"
|
||||
}
|
||||
}
|
||||
28
examples/brainy-service-template/config/production.json
Normal file
28
examples/brainy-service-template/config/production.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"service": {
|
||||
"cors": {
|
||||
"origins": ["https://yourdomain.com"]
|
||||
},
|
||||
"rateLimit": {
|
||||
"windowMs": 900000,
|
||||
"maxRequests": 1000
|
||||
}
|
||||
},
|
||||
"brainy": {
|
||||
"storage": {
|
||||
"type": "s3",
|
||||
"s3": {
|
||||
"bucketName": "${BRAINY_S3_BUCKET}",
|
||||
"region": "${BRAINY_S3_REGION}",
|
||||
"accessKeyId": "${BRAINY_S3_ACCESS_KEY}",
|
||||
"secretAccessKey": "${BRAINY_S3_SECRET_KEY}"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"hotCacheMaxSize": 50000
|
||||
}
|
||||
},
|
||||
"logging": {
|
||||
"level": "warn"
|
||||
}
|
||||
}
|
||||
35
examples/brainy-service-template/config/test.json
Normal file
35
examples/brainy-service-template/config/test.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"service": {
|
||||
"name": "brainy-service-test",
|
||||
"port": 0
|
||||
},
|
||||
"brainy": {
|
||||
"storage": {
|
||||
"type": "memory"
|
||||
},
|
||||
"features": {
|
||||
"intelligentVerbScoring": true,
|
||||
"realTimeUpdates": false,
|
||||
"distributedMode": false
|
||||
},
|
||||
"intelligentVerbScoring": {
|
||||
"enabled": true,
|
||||
"enableSemanticScoring": true,
|
||||
"enableFrequencyAmplification": true,
|
||||
"enableTemporalDecay": false,
|
||||
"baseConfidence": 0.6,
|
||||
"learningRate": 0.1
|
||||
},
|
||||
"cache": {
|
||||
"autoTune": false,
|
||||
"hotCacheMaxSize": 1000
|
||||
},
|
||||
"logging": {
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
"logging": {
|
||||
"level": "error",
|
||||
"format": "json"
|
||||
}
|
||||
}
|
||||
77
examples/brainy-service-template/docker-compose.yml
Normal file
77
examples/brainy-service-template/docker-compose.yml
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
brainy-service:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- LOG_LEVEL=info
|
||||
volumes:
|
||||
- brainy_data:/app/data
|
||||
- ./logs:/app/logs
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health/liveness"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
restart: unless-stopped
|
||||
|
||||
# Example with S3-compatible storage (MinIO)
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
environment:
|
||||
- MINIO_ROOT_USER=minioadmin
|
||||
- MINIO_ROOT_PASSWORD=minioadmin123
|
||||
command: server /data --console-address ":9001"
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
profiles:
|
||||
- s3
|
||||
|
||||
# Brainy service with S3 storage
|
||||
brainy-service-s3:
|
||||
build: .
|
||||
ports:
|
||||
- "3001:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- LOG_LEVEL=info
|
||||
- BRAINY_STORAGE_TYPE=s3
|
||||
- BRAINY_S3_BUCKET=brainy-data
|
||||
- BRAINY_S3_ENDPOINT=http://minio:9000
|
||||
- BRAINY_S3_ACCESS_KEY_ID=minioadmin
|
||||
- BRAINY_S3_SECRET_ACCESS_KEY=minioadmin123
|
||||
- BRAINY_S3_REGION=us-east-1
|
||||
depends_on:
|
||||
minio:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health/liveness"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
restart: unless-stopped
|
||||
profiles:
|
||||
- s3
|
||||
|
||||
volumes:
|
||||
brainy_data:
|
||||
driver: local
|
||||
minio_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
84
examples/brainy-service-template/package.json
Normal file
84
examples/brainy-service-template/package.json
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
{
|
||||
"name": "brainy-service-template",
|
||||
"version": "2.0.0",
|
||||
"description": "Zero-configuration, intelligent service template using Brainy's native augmentation system",
|
||||
"main": "src/app.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node src/app.js",
|
||||
"dev": "nodemon src/app.js --ignore tests/",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"build": "echo 'No build step required - zero configuration!'",
|
||||
"lint": "eslint src/ --ext .js",
|
||||
"format": "prettier --write src/",
|
||||
"docker:build": "docker build -t brainy-service .",
|
||||
"docker:run": "docker run -p 3000:3000 -p 3001:3001 -p 3002:3002 brainy-service",
|
||||
"docker:compose": "docker-compose up",
|
||||
"download-models": "node -e \"import('@soulcraft/brainy').then(brainy => brainy.downloadModels())\"",
|
||||
"health-check": "curl -f http://localhost:3000/health || exit 1"
|
||||
},
|
||||
"keywords": [
|
||||
"brainy",
|
||||
"vector-database",
|
||||
"graph-database",
|
||||
"semantic-search",
|
||||
"ai",
|
||||
"machine-learning",
|
||||
"template",
|
||||
"service",
|
||||
"augmentation",
|
||||
"websocket",
|
||||
"webrtc",
|
||||
"zero-config",
|
||||
"intelligent-adaptation",
|
||||
"real-time",
|
||||
"peer-to-peer",
|
||||
"universal-compatibility"
|
||||
],
|
||||
"dependencies": {
|
||||
"@soulcraft/brainy": "^0.51.2",
|
||||
"ws": "^8.14.2",
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"compression": "^1.7.4",
|
||||
"winston": "^3.11.0",
|
||||
"config": "^3.3.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^1.0.4",
|
||||
"supertest": "^6.3.3",
|
||||
"@vitest/coverage-v8": "^1.0.4",
|
||||
"nodemon": "^3.0.2",
|
||||
"eslint": "^8.56.0",
|
||||
"prettier": "^3.1.1"
|
||||
},
|
||||
"brainyConfig": {
|
||||
"augmentations": [
|
||||
"websocket",
|
||||
"webrtc",
|
||||
"http",
|
||||
"autoDiscovery",
|
||||
"adaptiveStorage",
|
||||
"environmentAdapter"
|
||||
],
|
||||
"transports": {
|
||||
"websocket": { "port": 3001, "realtime": true },
|
||||
"webrtc": { "port": 3002, "peerToPeer": true },
|
||||
"http": { "port": 3000, "minimal": true }
|
||||
},
|
||||
"features": {
|
||||
"zeroConfiguration": true,
|
||||
"intelligentAdaptation": true,
|
||||
"intelligentVerbScoring": true,
|
||||
"autoDiscovery": true,
|
||||
"adaptivePerformance": true
|
||||
}
|
||||
},
|
||||
"author": "David Snelling (david@soulcraft.com)",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
431
examples/brainy-service-template/src/app.js
Normal file
431
examples/brainy-service-template/src/app.js
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
import { BrainyData } from '@soulcraft/brainy'
|
||||
import { logger } from './utils/logger.js'
|
||||
|
||||
// Brainy-native augmentations
|
||||
import { WebSocketAugmentation } from './augmentations/websocketAugmentation.js'
|
||||
import { WebRTCAugmentation } from './augmentations/webrtcAugmentation.js'
|
||||
import { HttpAugmentation } from './augmentations/httpAugmentation.js'
|
||||
import { AutoDiscoveryAugmentation } from './augmentations/autoDiscoveryAugmentation.js'
|
||||
import { AdaptiveStorageAugmentation } from './augmentations/adaptiveStorageAugmentation.js'
|
||||
import { EnvironmentAdapterAugmentation } from './augmentations/environmentAdapterAugmentation.js'
|
||||
|
||||
/**
|
||||
* Zero-configuration Brainy service template that adapts to any environment
|
||||
* Uses Brainy's native augmentation system for all functionality
|
||||
*/
|
||||
class BrainyServiceTemplate {
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
// Zero configuration by default - everything auto-detected
|
||||
autoDetectEverything: true,
|
||||
|
||||
// Intelligent adaptation enabled
|
||||
intelligentAdaptation: true,
|
||||
|
||||
// Communication preferences (auto-detected if not specified)
|
||||
preferredTransports: options.transports || 'auto', // 'auto', 'websocket', 'webrtc', 'http'
|
||||
|
||||
// Storage preference (auto-detected if not specified)
|
||||
storagePreference: options.storage || 'auto', // 'auto', 'memory', 'filesystem', 's3', 'distributed'
|
||||
|
||||
// Environment adaptation
|
||||
adaptToEnvironment: true,
|
||||
|
||||
...options
|
||||
}
|
||||
|
||||
this.db = null
|
||||
this.isInitialized = false
|
||||
this.activeTransports = []
|
||||
this.capabilities = null
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
if (this.isInitialized) {
|
||||
return this.db
|
||||
}
|
||||
|
||||
logger.info('🧠 Initializing Brainy Service Template with zero configuration...')
|
||||
|
||||
try {
|
||||
// Step 1: Detect environment capabilities
|
||||
this.capabilities = await this.detectEnvironmentCapabilities()
|
||||
logger.info('Environment capabilities detected', this.capabilities)
|
||||
|
||||
// Step 2: Build intelligent Brainy configuration
|
||||
const brainyConfig = await this.buildIntelligentBrainyConfig()
|
||||
|
||||
// Step 3: Initialize Brainy with intelligent defaults
|
||||
this.db = new BrainyData(brainyConfig)
|
||||
await this.db.init()
|
||||
|
||||
// Step 4: Add environment-specific augmentations
|
||||
await this.addIntelligentAugmentations()
|
||||
|
||||
this.isInitialized = true
|
||||
|
||||
logger.info('🚀 Brainy Service Template ready!', {
|
||||
storage: brainyConfig.storage?.type || 'auto-detected',
|
||||
transports: this.activeTransports,
|
||||
environment: this.capabilities.environment,
|
||||
augmentations: this.db.augmentations?.length || 0
|
||||
})
|
||||
|
||||
return this.db
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize Brainy Service Template:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async detectEnvironmentCapabilities() {
|
||||
const capabilities = {
|
||||
// Environment detection
|
||||
environment: this.detectEnvironmentType(),
|
||||
|
||||
// Memory and performance
|
||||
memory: {
|
||||
available: Math.floor(process.memoryUsage().heapTotal / 1024 / 1024), // MB
|
||||
isHighMemory: process.memoryUsage().heapTotal > 500 * 1024 * 1024
|
||||
},
|
||||
|
||||
// Network capabilities
|
||||
network: await this.detectNetworkCapabilities(),
|
||||
|
||||
// Storage capabilities
|
||||
storage: await this.detectStorageCapabilities(),
|
||||
|
||||
// Runtime characteristics
|
||||
runtime: {
|
||||
isLongRunning: process.env.NODE_ENV === 'production' || !!process.env.KUBERNETES_SERVICE_HOST,
|
||||
isPersistent: !this.isServerlessEnvironment(),
|
||||
supportsWorkers: typeof Worker !== 'undefined' || typeof require !== 'undefined'
|
||||
},
|
||||
|
||||
// Platform detection
|
||||
platform: {
|
||||
isNode: typeof process !== 'undefined',
|
||||
isBrowser: typeof window !== 'undefined',
|
||||
isEdge: this.isEdgeEnvironment(),
|
||||
isServerless: this.isServerlessEnvironment(),
|
||||
isContainer: this.isContainerEnvironment()
|
||||
}
|
||||
}
|
||||
|
||||
return capabilities
|
||||
}
|
||||
|
||||
detectEnvironmentType() {
|
||||
if (typeof window !== 'undefined') return 'browser'
|
||||
if (process.env.VERCEL || process.env.NETLIFY) return 'serverless'
|
||||
if (process.env.KUBERNETES_SERVICE_HOST) return 'kubernetes'
|
||||
if (process.env.AWS_LAMBDA_FUNCTION_NAME) return 'lambda'
|
||||
if (this.isContainerEnvironment()) return 'container'
|
||||
return 'node'
|
||||
}
|
||||
|
||||
async detectNetworkCapabilities() {
|
||||
return {
|
||||
supportsWebSocket: true, // Available in most environments
|
||||
supportsWebRTC: typeof RTCPeerConnection !== 'undefined' || this.capabilities?.platform?.isNode,
|
||||
supportsHTTP: true,
|
||||
supportsPeerToPeer: !this.isServerlessEnvironment(),
|
||||
hasPublicIP: !this.isPrivateNetwork(),
|
||||
preferredPort: this.detectPreferredPort()
|
||||
}
|
||||
}
|
||||
|
||||
async detectStorageCapabilities() {
|
||||
const capabilities = {
|
||||
memory: true,
|
||||
filesystem: false,
|
||||
s3: false,
|
||||
distributed: false
|
||||
}
|
||||
|
||||
// Test filesystem access
|
||||
try {
|
||||
if (typeof require !== 'undefined') {
|
||||
const fs = require('fs')
|
||||
fs.accessSync('.', fs.constants.W_OK)
|
||||
capabilities.filesystem = true
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Detect S3 capability
|
||||
capabilities.s3 = !!(process.env.AWS_REGION || process.env.S3_BUCKET || process.env.BRAINY_S3_BUCKET)
|
||||
|
||||
// Detect distributed storage capability
|
||||
capabilities.distributed = this.capabilities?.platform?.isContainer || this.capabilities?.environment === 'kubernetes'
|
||||
|
||||
return capabilities
|
||||
}
|
||||
|
||||
async buildIntelligentBrainyConfig() {
|
||||
const config = {}
|
||||
|
||||
// Intelligent storage selection
|
||||
config.storage = await this.selectOptimalStorage()
|
||||
|
||||
// Intelligent verb scoring - always enabled with adaptive parameters
|
||||
config.intelligentVerbScoring = {
|
||||
enabled: true,
|
||||
enableSemanticScoring: true,
|
||||
enableFrequencyAmplification: true,
|
||||
enableTemporalDecay: this.capabilities.runtime.isLongRunning,
|
||||
learningRate: this.capabilities.memory.isHighMemory ? 0.15 : 0.1,
|
||||
baseConfidence: 0.6,
|
||||
adaptiveParameters: true
|
||||
}
|
||||
|
||||
// Adaptive caching based on environment
|
||||
config.cache = {
|
||||
autoTune: true,
|
||||
hotCacheMaxSize: this.calculateOptimalCacheSize(),
|
||||
adaptiveEviction: true,
|
||||
persistentCache: this.capabilities.storage.filesystem
|
||||
}
|
||||
|
||||
// Real-time updates for persistent environments
|
||||
if (this.capabilities.runtime.isPersistent) {
|
||||
config.realtimeUpdates = {
|
||||
enabled: true,
|
||||
interval: this.capabilities.memory.isHighMemory ? 15000 : 30000,
|
||||
updateStatistics: true,
|
||||
updateIndex: true
|
||||
}
|
||||
}
|
||||
|
||||
// Distributed mode for container/cloud environments
|
||||
if (this.capabilities.storage.distributed) {
|
||||
config.distributed = true
|
||||
}
|
||||
|
||||
// Performance optimizations
|
||||
config.performance = {
|
||||
adaptiveBatching: true,
|
||||
intelligentIndexing: true,
|
||||
backgroundOptimization: this.capabilities.runtime.isLongRunning,
|
||||
workerThreads: this.capabilities.runtime.supportsWorkers
|
||||
}
|
||||
|
||||
logger.debug('Built intelligent Brainy configuration', config)
|
||||
return config
|
||||
}
|
||||
|
||||
async selectOptimalStorage() {
|
||||
// S3/Cloud storage for production cloud environments
|
||||
if (this.capabilities.storage.s3 && this.capabilities.environment !== 'development') {
|
||||
return {
|
||||
type: 's3',
|
||||
s3Storage: {
|
||||
bucketName: process.env.BRAINY_S3_BUCKET || process.env.S3_BUCKET || 'brainy-data',
|
||||
region: process.env.AWS_REGION || process.env.BRAINY_S3_REGION || 'us-east-1',
|
||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID || process.env.BRAINY_S3_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || process.env.BRAINY_S3_SECRET_ACCESS_KEY,
|
||||
endpoint: process.env.BRAINY_S3_ENDPOINT || process.env.S3_ENDPOINT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filesystem for persistent environments
|
||||
if (this.capabilities.storage.filesystem && this.capabilities.runtime.isPersistent) {
|
||||
const dataPath = process.env.BRAINY_DATA_PATH || './data'
|
||||
try {
|
||||
const fs = require('fs')
|
||||
if (!fs.existsSync(dataPath)) {
|
||||
fs.mkdirSync(dataPath, { recursive: true })
|
||||
}
|
||||
return { type: 'filesystem', path: dataPath }
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Memory for serverless/temporary environments
|
||||
return { type: 'memory' }
|
||||
}
|
||||
|
||||
calculateOptimalCacheSize() {
|
||||
const availableMemory = this.capabilities.memory.available
|
||||
|
||||
if (availableMemory > 2000) return 100000 // High memory
|
||||
if (availableMemory > 1000) return 50000 // Medium memory
|
||||
if (availableMemory > 500) return 25000 // Low memory
|
||||
return 10000 // Very low memory
|
||||
}
|
||||
|
||||
async addIntelligentAugmentations() {
|
||||
// Environment adapter - always first
|
||||
const envAdapter = new EnvironmentAdapterAugmentation(this.capabilities)
|
||||
await this.db.addAugmentation(envAdapter)
|
||||
logger.info('Added environment adapter augmentation')
|
||||
|
||||
// Adaptive storage - optimizes storage usage
|
||||
const adaptiveStorage = new AdaptiveStorageAugmentation(this.capabilities)
|
||||
await this.db.addAugmentation(adaptiveStorage)
|
||||
logger.info('Added adaptive storage augmentation')
|
||||
|
||||
// Auto-discovery - helps users understand their data
|
||||
const autoDiscovery = new AutoDiscoveryAugmentation()
|
||||
await this.db.addAugmentation(autoDiscovery)
|
||||
logger.info('Added auto-discovery augmentation')
|
||||
|
||||
// Add transport augmentations based on capabilities and preferences
|
||||
await this.addTransportAugmentations()
|
||||
}
|
||||
|
||||
async addTransportAugmentations() {
|
||||
const transports = this.determineOptimalTransports()
|
||||
|
||||
for (const transport of transports) {
|
||||
try {
|
||||
switch (transport.type) {
|
||||
case 'webrtc':
|
||||
if (this.capabilities.network.supportsWebRTC && this.capabilities.network.supportsPeerToPeer) {
|
||||
const webrtc = new WebRTCAugmentation({
|
||||
port: transport.port,
|
||||
enableDataChannels: true,
|
||||
enablePeerDiscovery: true
|
||||
})
|
||||
await this.db.addAugmentation(webrtc)
|
||||
this.activeTransports.push('webrtc')
|
||||
logger.info(`WebRTC augmentation active on port ${transport.port}`)
|
||||
}
|
||||
break
|
||||
|
||||
case 'websocket':
|
||||
if (this.capabilities.network.supportsWebSocket) {
|
||||
const websocket = new WebSocketAugmentation({
|
||||
port: transport.port,
|
||||
enableRealtime: true,
|
||||
enableBroadcast: true
|
||||
})
|
||||
await this.db.addAugmentation(websocket)
|
||||
this.activeTransports.push('websocket')
|
||||
logger.info(`WebSocket augmentation active on port ${transport.port}`)
|
||||
}
|
||||
break
|
||||
|
||||
case 'http':
|
||||
const http = new HttpAugmentation({
|
||||
port: transport.port,
|
||||
enableCORS: true,
|
||||
enableCompression: true,
|
||||
minimal: true // Keep it lightweight
|
||||
})
|
||||
await this.db.addAugmentation(http)
|
||||
this.activeTransports.push('http')
|
||||
logger.info(`HTTP augmentation active on port ${transport.port}`)
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to add ${transport.type} transport:`, error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
determineOptimalTransports() {
|
||||
const transports = []
|
||||
const basePort = this.capabilities.network.preferredPort
|
||||
|
||||
// WebRTC for peer-to-peer capable environments (best performance)
|
||||
if (this.capabilities.network.supportsWebRTC &&
|
||||
this.capabilities.network.supportsPeerToPeer &&
|
||||
this.options.preferredTransports !== 'http') {
|
||||
transports.push({ type: 'webrtc', port: basePort + 2, priority: 1 })
|
||||
}
|
||||
|
||||
// WebSocket for real-time environments (good performance)
|
||||
if (this.capabilities.network.supportsWebSocket &&
|
||||
this.capabilities.runtime.isPersistent &&
|
||||
this.options.preferredTransports !== 'http') {
|
||||
transports.push({ type: 'websocket', port: basePort + 1, priority: 2 })
|
||||
}
|
||||
|
||||
// HTTP as fallback (universal compatibility)
|
||||
transports.push({ type: 'http', port: basePort, priority: 3 })
|
||||
|
||||
return transports.sort((a, b) => a.priority - b.priority)
|
||||
}
|
||||
|
||||
detectPreferredPort() {
|
||||
return parseInt(process.env.PORT || process.env.BRAINY_PORT || 3000)
|
||||
}
|
||||
|
||||
isServerlessEnvironment() {
|
||||
return !!(process.env.VERCEL || process.env.NETLIFY || process.env.AWS_LAMBDA_FUNCTION_NAME)
|
||||
}
|
||||
|
||||
isEdgeEnvironment() {
|
||||
return !!(process.env.VERCEL_REGION || process.env.CF_PAGES)
|
||||
}
|
||||
|
||||
isContainerEnvironment() {
|
||||
try {
|
||||
const fs = require('fs')
|
||||
return fs.existsSync('/.dockerenv') || !!process.env.KUBERNETES_SERVICE_HOST
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
isPrivateNetwork() {
|
||||
// Simple heuristic for private network detection
|
||||
return !!(process.env.KUBERNETES_SERVICE_HOST || process.env.DOCKER_CONTAINER)
|
||||
}
|
||||
|
||||
// Public API
|
||||
getDatabase() {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error('Service not initialized. Call initialize() first.')
|
||||
}
|
||||
return this.db
|
||||
}
|
||||
|
||||
getCapabilities() {
|
||||
return this.capabilities
|
||||
}
|
||||
|
||||
getActiveTransports() {
|
||||
return this.activeTransports
|
||||
}
|
||||
|
||||
async shutdown() {
|
||||
if (this.db && this.db.cleanup) {
|
||||
await this.db.cleanup()
|
||||
}
|
||||
this.isInitialized = false
|
||||
logger.info('🔄 Brainy Service Template shutdown complete')
|
||||
}
|
||||
}
|
||||
|
||||
// Factory function for easy usage
|
||||
export const createBrainyService = async (options = {}) => {
|
||||
const service = new BrainyServiceTemplate(options)
|
||||
await service.initialize()
|
||||
return service
|
||||
}
|
||||
|
||||
// Default export
|
||||
export default BrainyServiceTemplate
|
||||
|
||||
// Auto-start if run directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
const service = new BrainyServiceTemplate()
|
||||
|
||||
const gracefulShutdown = async (signal) => {
|
||||
logger.info(`Received ${signal}. Starting graceful shutdown...`)
|
||||
await service.shutdown()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'))
|
||||
process.on('SIGINT', () => gracefulShutdown('SIGINT'))
|
||||
|
||||
try {
|
||||
await service.initialize()
|
||||
logger.info('🎉 Brainy Service Template is running! Use Ctrl+C to stop.')
|
||||
} catch (error) {
|
||||
logger.error('Failed to start:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,165 @@
|
|||
import { WebSocket, WebSocketServer } from 'ws'
|
||||
import { logger } from '../utils/logger.js'
|
||||
|
||||
/**
|
||||
* WebSocket Augmentation - Real-time communication using Brainy's native augmentation system
|
||||
* Handles real-time queries, updates, and bidirectional communication
|
||||
*/
|
||||
export class WebSocketAugmentation {
|
||||
constructor(options = {}) {
|
||||
this.type = 'WEBSOCKET'
|
||||
this.priority = 2
|
||||
this.options = {
|
||||
port: options.port || 3001,
|
||||
enableRealtime: options.enableRealtime !== false,
|
||||
enableBroadcast: options.enableBroadcast !== false,
|
||||
heartbeatInterval: options.heartbeatInterval || 30000,
|
||||
...options
|
||||
}
|
||||
|
||||
this.server = null
|
||||
this.clients = new Set()
|
||||
this.db = null
|
||||
this.heartbeatTimer = null
|
||||
}
|
||||
|
||||
async augment(brainyData, context) {
|
||||
this.db = brainyData
|
||||
|
||||
try {
|
||||
// Create WebSocket server
|
||||
this.server = new WebSocketServer({
|
||||
port: this.options.port,
|
||||
perMessageDeflate: true
|
||||
})
|
||||
|
||||
this.server.on('connection', (ws, request) => {
|
||||
this.handleConnection(ws, request)
|
||||
})
|
||||
|
||||
// Start heartbeat for connection management
|
||||
if (this.options.heartbeatInterval > 0) {
|
||||
this.startHeartbeat()
|
||||
}
|
||||
|
||||
logger.info('WebSocket augmentation initialized', {
|
||||
port: this.options.port,
|
||||
features: {
|
||||
realtime: this.options.enableRealtime,
|
||||
broadcast: this.options.enableBroadcast
|
||||
}
|
||||
})
|
||||
|
||||
// Hook into Brainy events if real-time is enabled
|
||||
if (this.options.enableRealtime) {
|
||||
this.setupRealtimeHooks()
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize WebSocket augmentation:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
handleConnection(ws, request) {
|
||||
const clientId = this.generateClientId()
|
||||
ws.clientId = clientId
|
||||
this.clients.add(ws)
|
||||
|
||||
logger.debug('WebSocket client connected', {
|
||||
clientId,
|
||||
ip: request.socket.remoteAddress,
|
||||
totalClients: this.clients.size
|
||||
})
|
||||
|
||||
// Send welcome message
|
||||
this.send(ws, {
|
||||
type: 'welcome',
|
||||
clientId,
|
||||
capabilities: {
|
||||
realtime: this.options.enableRealtime,
|
||||
broadcast: this.options.enableBroadcast,
|
||||
commands: ['search', 'add', 'addVerb', 'get', 'subscribe', 'unsubscribe']
|
||||
}
|
||||
})
|
||||
|
||||
ws.on('message', (data) => {
|
||||
this.handleMessage(ws, data)
|
||||
})
|
||||
|
||||
ws.on('close', () => {
|
||||
this.clients.delete(ws)
|
||||
logger.debug('WebSocket client disconnected', {
|
||||
clientId,
|
||||
totalClients: this.clients.size
|
||||
})
|
||||
})
|
||||
|
||||
ws.on('error', (error) => {
|
||||
logger.warn('WebSocket client error:', { clientId, error: error.message })
|
||||
})
|
||||
|
||||
// Keep connection alive
|
||||
ws.isAlive = true
|
||||
ws.on('pong', () => {
|
||||
ws.isAlive = true
|
||||
})
|
||||
}
|
||||
|
||||
async handleMessage(ws, data) {
|
||||
try {
|
||||
const message = JSON.parse(data.toString())
|
||||
const { id, type, payload } = message
|
||||
|
||||
let result
|
||||
|
||||
switch (type) {
|
||||
case 'search':
|
||||
result = await this.handleSearch(payload)
|
||||
break
|
||||
|
||||
case 'add':
|
||||
result = await this.handleAdd(payload)
|
||||
break
|
||||
|
||||
case 'addVerb':
|
||||
result = await this.handleAddVerb(payload)
|
||||
break
|
||||
|
||||
case 'get':
|
||||
result = await this.handleGet(payload)
|
||||
break
|
||||
|
||||
case 'subscribe':
|
||||
result = await this.handleSubscribe(ws, payload)
|
||||
break
|
||||
|
||||
case 'unsubscribe':
|
||||
result = await this.handleUnsubscribe(ws, payload)
|
||||
break
|
||||
|
||||
case 'ping':
|
||||
result = { pong: Date.now() }
|
||||
break
|
||||
|
||||
default:\n throw new Error(`Unknown message type: ${type}`)
|
||||
}
|
||||
|
||||
// Send response
|
||||
this.send(ws, {\n id,\n type: 'response',\n payload: result\n })
|
||||
|
||||
} catch (error) {\n logger.warn('WebSocket message handling error:', error)\n this.send(ws, {\n id: message?.id,\n type: 'error',\n payload: {\n message: error.message,\n code: error.code || 'UNKNOWN_ERROR'\n }\n })\n }\n }
|
||||
|
||||
async handleSearch(payload) {\n const { query, limit = 10, threshold = 0.7, options = {} } = payload\n \n if (!query) {\n throw new Error('Query is required for search')\n }\n\n const results = await this.db.search(query, limit, { threshold, ...options })\n \n return {\n query,\n results,\n count: results.length,\n timestamp: new Date().toISOString()\n }\n }
|
||||
|
||||
async handleAdd(payload) {\n const { data, metadata = {}, options = {} } = payload\n \n if (!data) {\n throw new Error('Data is required for add operation')\n }\n\n const enhancedMetadata = {\n ...metadata,\n source: 'websocket',\n timestamp: new Date().toISOString()\n }\n\n const id = await this.db.add(data, enhancedMetadata, options)\n \n const result = {\n id,\n data,\n metadata: enhancedMetadata\n }\n\n // Broadcast to subscribers if enabled\n if (this.options.enableBroadcast) {\n this.broadcast({\n type: 'entityAdded',\n payload: result\n })\n }\n\n return result\n }
|
||||
|
||||
async handleAddVerb(payload) {\n const { sourceId, targetId, verbType, weight, metadata = {}, options = {} } = payload\n \n if (!sourceId || !targetId || !verbType) {\n throw new Error('sourceId, targetId, and verbType are required')\n }\n\n const enhancedMetadata = {\n ...metadata,\n source: 'websocket',\n timestamp: new Date().toISOString()\n }\n\n const verbId = await this.db.addVerb(sourceId, targetId, verbType, {\n weight,\n metadata: enhancedMetadata,\n ...options\n })\n \n const result = {\n id: verbId,\n sourceId,\n targetId,\n verbType,\n weight,\n metadata: enhancedMetadata\n }\n\n // Broadcast to subscribers if enabled\n if (this.options.enableBroadcast) {\n this.broadcast({\n type: 'verbAdded',\n payload: result\n })\n }\n\n return result\n }
|
||||
|
||||
async handleGet(payload) {\n const { id } = payload\n \n if (!id) {\n throw new Error('ID is required for get operation')\n }\n\n const entity = await this.db.get(id)\n \n if (!entity) {\n throw new Error(`Entity with ID ${id} not found`)\n }\n\n return entity\n }
|
||||
|
||||
async handleSubscribe(ws, payload) {\n const { events = ['entityAdded', 'verbAdded'] } = payload\n \n if (!ws.subscriptions) {\n ws.subscriptions = new Set()\n }\n \n events.forEach(event => ws.subscriptions.add(event))\n \n return {\n subscribed: Array.from(ws.subscriptions),\n message: 'Successfully subscribed to events'\n }\n }
|
||||
|
||||
async handleUnsubscribe(ws, payload) {\n const { events = [] } = payload\n \n if (!ws.subscriptions) {\n return { message: 'No active subscriptions' }\n }\n \n if (events.length === 0) {\n // Unsubscribe from all\n ws.subscriptions.clear()\n } else {\n events.forEach(event => ws.subscriptions.delete(event))\n }\n \n return {\n subscribed: Array.from(ws.subscriptions),\n message: 'Successfully unsubscribed from events'\n }\n }
|
||||
|
||||
setupRealtimeHooks() {\n // Hook into Brainy's internal events for real-time updates\n // This would integrate with Brainy's event system once available\n logger.debug('Real-time hooks configured for WebSocket augmentation')\n }\n\n broadcast(message, filter = null) {\n if (!this.options.enableBroadcast) return\n \n const data = JSON.stringify(message)\n \n this.clients.forEach(client => {\n if (client.readyState === WebSocket.OPEN) {\n // Check if client is subscribed to this event type\n if (client.subscriptions && client.subscriptions.has(message.type)) {\n // Apply filter if provided\n if (!filter || filter(client)) {\n client.send(data)\n }\n }\n }\n })\n }\n\n send(ws, message) {\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify(message))\n }\n }\n\n startHeartbeat() {\n this.heartbeatTimer = setInterval(() => {\n this.clients.forEach(ws => {\n if (!ws.isAlive) {\n ws.terminate()\n return\n }\n \n ws.isAlive = false\n ws.ping()\n })\n }, this.options.heartbeatInterval)\n }\n\n generateClientId() {\n return `ws_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`\n }\n\n async cleanup() {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer)\n }\n \n if (this.server) {\n this.server.close()\n }\n \n // Close all client connections\n this.clients.forEach(client => {\n client.close()\n })\n this.clients.clear()\n \n logger.info('WebSocket augmentation cleaned up')\n }\n}
|
||||
174
examples/brainy-service-template/src/controllers/entities.js
Normal file
174
examples/brainy-service-template/src/controllers/entities.js
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import { logger } from '../utils/logger.js'
|
||||
import { ApiError } from '../utils/errors.js'
|
||||
|
||||
export const entitiesController = {
|
||||
async create(req, res, next) {
|
||||
try {
|
||||
const { data, metadata = {}, options = {} } = req.body
|
||||
const { brainyService } = req.app.locals
|
||||
|
||||
if (!data) {
|
||||
throw new ApiError(400, 'Data is required')
|
||||
}
|
||||
|
||||
// Add timestamp and source info to metadata
|
||||
const enhancedMetadata = {
|
||||
...metadata,
|
||||
createdAt: new Date().toISOString(),
|
||||
source: 'api'
|
||||
}
|
||||
|
||||
const entityId = await brainyService.addEntity(data, enhancedMetadata, options)
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
id: entityId,
|
||||
data: {
|
||||
id: entityId,
|
||||
data,
|
||||
metadata: enhancedMetadata
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
},
|
||||
|
||||
async getById(req, res, next) {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const { brainyService } = req.app.locals
|
||||
|
||||
const entity = await brainyService.getEntity(id)
|
||||
|
||||
if (!entity) {
|
||||
throw new ApiError(404, `Entity with ID ${id} not found`)
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: entity
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
},
|
||||
|
||||
async update(req, res, next) {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const { data, metadata = {} } = req.body
|
||||
const { brainyService } = req.app.locals
|
||||
|
||||
// Check if entity exists
|
||||
const existingEntity = await brainyService.getEntity(id)
|
||||
if (!existingEntity) {
|
||||
throw new ApiError(404, `Entity with ID ${id} not found`)
|
||||
}
|
||||
|
||||
// Add update timestamp
|
||||
const enhancedMetadata = {
|
||||
...metadata,
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
await brainyService.updateEntity(id, data, enhancedMetadata)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Entity updated successfully',
|
||||
id
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
},
|
||||
|
||||
async delete(req, res, next) {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const { brainyService } = req.app.locals
|
||||
|
||||
// Check if entity exists
|
||||
const existingEntity = await brainyService.getEntity(id)
|
||||
if (!existingEntity) {
|
||||
throw new ApiError(404, `Entity with ID ${id} not found`)
|
||||
}
|
||||
|
||||
await brainyService.deleteEntity(id)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Entity deleted successfully',
|
||||
id
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
},
|
||||
|
||||
async search(req, res, next) {
|
||||
try {
|
||||
const { query, limit = 10, threshold = 0.7, includeMetadata = true } = req.body
|
||||
const { brainyService } = req.app.locals
|
||||
|
||||
if (!query) {
|
||||
throw new ApiError(400, 'Query is required')
|
||||
}
|
||||
|
||||
const options = {
|
||||
limit: Math.min(limit, 100), // Cap at 100 results
|
||||
threshold,
|
||||
includeMetadata
|
||||
}
|
||||
|
||||
const results = await brainyService.searchEntities(query, options)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
query,
|
||||
results: results.map(result => ({
|
||||
id: result.id,
|
||||
similarity: result.similarity,
|
||||
data: result.data,
|
||||
metadata: result.metadata
|
||||
})),
|
||||
count: results.length,
|
||||
options
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
},
|
||||
|
||||
async list(req, res, next) {
|
||||
try {
|
||||
const { page = 1, limit = 50, type } = req.query
|
||||
const { brainyService } = req.app.locals
|
||||
|
||||
const options = {
|
||||
offset: (page - 1) * limit,
|
||||
limit: Math.min(limit, 100)
|
||||
}
|
||||
|
||||
// Add type filter if specified
|
||||
if (type) {
|
||||
options.filter = { type }
|
||||
}
|
||||
|
||||
const entities = await brainyService.listEntities(options)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: entities,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
count: entities.length
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
130
examples/brainy-service-template/src/controllers/health.js
Normal file
130
examples/brainy-service-template/src/controllers/health.js
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import { logger } from '../utils/logger.js'
|
||||
|
||||
export const healthController = {
|
||||
async check(req, res, next) {
|
||||
try {
|
||||
const { brainyService } = req.app.locals
|
||||
|
||||
// Get Brainy health status
|
||||
const brainyHealth = await brainyService.getHealth()
|
||||
|
||||
// Overall system health
|
||||
const health = {
|
||||
status: brainyHealth.status === 'healthy' ? 'healthy' : 'unhealthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: process.env.npm_package_version || '1.0.0',
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
uptime: Math.floor(process.uptime()),
|
||||
services: {
|
||||
brainy: brainyHealth
|
||||
},
|
||||
system: {
|
||||
memory: {
|
||||
used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
||||
total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024),
|
||||
external: Math.round(process.memoryUsage().external / 1024 / 1024)
|
||||
},
|
||||
cpu: process.cpuUsage()
|
||||
}
|
||||
}
|
||||
|
||||
const statusCode = health.status === 'healthy' ? 200 : 503
|
||||
|
||||
res.status(statusCode).json(health)
|
||||
} catch (error) {
|
||||
logger.error('Health check failed:', error)
|
||||
|
||||
res.status(503).json({
|
||||
status: 'unhealthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
error: error.message,
|
||||
services: {
|
||||
brainy: { status: 'error', error: error.message }
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async readiness(req, res, next) {
|
||||
try {
|
||||
const { brainyService } = req.app.locals
|
||||
|
||||
// Check if services are ready
|
||||
const brainyHealth = await brainyService.getHealth()
|
||||
const isReady = brainyHealth.status === 'healthy'
|
||||
|
||||
const readiness = {
|
||||
ready: isReady,
|
||||
timestamp: new Date().toISOString(),
|
||||
checks: {
|
||||
brainy: {
|
||||
ready: brainyHealth.status === 'healthy',
|
||||
details: brainyHealth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const statusCode = readiness.ready ? 200 : 503
|
||||
res.status(statusCode).json(readiness)
|
||||
} catch (error) {
|
||||
logger.error('Readiness check failed:', error)
|
||||
|
||||
res.status(503).json({
|
||||
ready: false,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: error.message
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async liveness(req, res, next) {
|
||||
try {
|
||||
// Simple liveness check - just verify the process is running
|
||||
res.status(200).json({
|
||||
alive: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
pid: process.pid,
|
||||
uptime: Math.floor(process.uptime())
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Liveness check failed:', error)
|
||||
|
||||
res.status(503).json({
|
||||
alive: false,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: error.message
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async metrics(req, res, next) {
|
||||
try {
|
||||
const { brainyService } = req.app.locals
|
||||
|
||||
// Get detailed metrics
|
||||
const brainyMetrics = await brainyService.getMetrics()
|
||||
|
||||
const metrics = {
|
||||
timestamp: new Date().toISOString(),
|
||||
brainy: brainyMetrics.database,
|
||||
system: {
|
||||
...brainyMetrics.performance,
|
||||
process: {
|
||||
pid: process.pid,
|
||||
version: process.version,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
env: process.env.NODE_ENV || 'development'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: metrics
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
import { logger } from '../utils/logger.js'
|
||||
import { ApiError } from '../utils/errors.js'
|
||||
|
||||
export const relationshipsController = {
|
||||
async create(req, res, next) {
|
||||
try {
|
||||
const { sourceId, targetId, type, weight, metadata = {}, autoCreateMissingNouns = false } = req.body
|
||||
const { brainyService } = req.app.locals
|
||||
|
||||
if (!sourceId || !targetId || !type) {
|
||||
throw new ApiError(400, 'sourceId, targetId, and type are required')
|
||||
}
|
||||
|
||||
// Add timestamp and source info to metadata
|
||||
const enhancedMetadata = {
|
||||
...metadata,
|
||||
createdAt: new Date().toISOString(),
|
||||
source: 'api'
|
||||
}
|
||||
|
||||
const options = {
|
||||
weight,
|
||||
metadata: enhancedMetadata,
|
||||
autoCreateMissingNouns
|
||||
}
|
||||
|
||||
const relationshipId = await brainyService.addRelationship(sourceId, targetId, type, options)
|
||||
|
||||
// Get the created relationship to return full details
|
||||
const relationship = await brainyService.getRelationship(relationshipId)
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
id: relationshipId,
|
||||
data: {
|
||||
id: relationshipId,
|
||||
sourceId,
|
||||
targetId,
|
||||
type,
|
||||
weight: relationship?.metadata?.weight,
|
||||
confidence: relationship?.metadata?.confidence,
|
||||
intelligentScoring: relationship?.metadata?.intelligentScoring,
|
||||
metadata: enhancedMetadata
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
},
|
||||
|
||||
async getById(req, res, next) {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const { brainyService } = req.app.locals
|
||||
|
||||
const relationship = await brainyService.getRelationship(id)
|
||||
|
||||
if (!relationship) {
|
||||
throw new ApiError(404, `Relationship with ID ${id} not found`)
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: relationship
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
},
|
||||
|
||||
async update(req, res, next) {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const { weight, metadata = {} } = req.body
|
||||
const { brainyService } = req.app.locals
|
||||
|
||||
// Check if relationship exists
|
||||
const existingRelationship = await brainyService.getRelationship(id)
|
||||
if (!existingRelationship) {
|
||||
throw new ApiError(404, `Relationship with ID ${id} not found`)
|
||||
}
|
||||
|
||||
// Prepare updates
|
||||
const updates = {}
|
||||
if (weight !== undefined) updates.weight = weight
|
||||
if (Object.keys(metadata).length > 0) {
|
||||
updates.metadata = {
|
||||
...metadata,
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
await brainyService.updateRelationship(id, updates)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Relationship updated successfully',
|
||||
id
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
},
|
||||
|
||||
async delete(req, res, next) {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const { brainyService } = req.app.locals
|
||||
|
||||
// Check if relationship exists
|
||||
const existingRelationship = await brainyService.getRelationship(id)
|
||||
if (!existingRelationship) {
|
||||
throw new ApiError(404, `Relationship with ID ${id} not found`)
|
||||
}
|
||||
|
||||
await brainyService.deleteRelationship(id)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Relationship deleted successfully',
|
||||
id
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
},
|
||||
|
||||
async list(req, res, next) {
|
||||
try {
|
||||
const { page = 1, limit = 50, sourceId, targetId, type } = req.query
|
||||
const { brainyService } = req.app.locals
|
||||
|
||||
const options = {
|
||||
pagination: {
|
||||
offset: (page - 1) * limit,
|
||||
limit: Math.min(limit, 100)
|
||||
}
|
||||
}
|
||||
|
||||
// Add filters if specified
|
||||
const filter = {}
|
||||
if (sourceId) filter.sourceId = sourceId
|
||||
if (targetId) filter.targetId = targetId
|
||||
if (type) filter.verbType = type
|
||||
|
||||
if (Object.keys(filter).length > 0) {
|
||||
options.filter = filter
|
||||
}
|
||||
|
||||
const result = await brainyService.listRelationships(options)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.items || result,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
count: result.items ? result.items.length : result.length,
|
||||
hasMore: result.hasMore || false,
|
||||
totalCount: result.totalCount
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
195
examples/brainy-service-template/src/controllers/scoring.js
Normal file
195
examples/brainy-service-template/src/controllers/scoring.js
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
import { logger } from '../utils/logger.js'
|
||||
import { ApiError } from '../utils/errors.js'
|
||||
|
||||
export const scoringController = {
|
||||
async provideFeedback(req, res, next) {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const { weight, confidence, type = 'correction' } = req.body
|
||||
const { scoringService } = req.app.locals
|
||||
|
||||
if (!scoringService) {
|
||||
throw new ApiError(400, 'Intelligent verb scoring is not enabled')
|
||||
}
|
||||
|
||||
if (weight === undefined) {
|
||||
throw new ApiError(400, 'Weight is required for feedback')
|
||||
}
|
||||
|
||||
if (weight < 0 || weight > 1) {
|
||||
throw new ApiError(400, 'Weight must be between 0 and 1')
|
||||
}
|
||||
|
||||
if (confidence !== undefined && (confidence < 0 || confidence > 1)) {
|
||||
throw new ApiError(400, 'Confidence must be between 0 and 1')
|
||||
}
|
||||
|
||||
const feedbackData = {
|
||||
weight,
|
||||
confidence,
|
||||
type
|
||||
}
|
||||
|
||||
const result = await scoringService.provideFeedback(id, feedbackData)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: result.message,
|
||||
feedback: {
|
||||
relationshipId: id,
|
||||
weight,
|
||||
confidence,
|
||||
type,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
},
|
||||
|
||||
async getStats(req, res, next) {
|
||||
try {
|
||||
const { scoringService } = req.app.locals
|
||||
|
||||
if (!scoringService) {
|
||||
throw new ApiError(400, 'Intelligent verb scoring is not enabled')
|
||||
}
|
||||
|
||||
const stats = await scoringService.getStats()
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: stats
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
},
|
||||
|
||||
async exportLearningData(req, res, next) {
|
||||
try {
|
||||
const { scoringService } = req.app.locals
|
||||
|
||||
if (!scoringService) {
|
||||
throw new ApiError(400, 'Intelligent verb scoring is not enabled')
|
||||
}
|
||||
|
||||
const result = await scoringService.exportLearningData()
|
||||
|
||||
if (!result.data) {
|
||||
return res.json({
|
||||
success: true,
|
||||
message: result.message
|
||||
})
|
||||
}
|
||||
|
||||
// Set appropriate headers for download
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="brainy-learning-data.json"')
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
timestamp: result.timestamp,
|
||||
format: result.format
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
},
|
||||
|
||||
async importLearningData(req, res, next) {
|
||||
try {
|
||||
const { data } = req.body
|
||||
const { scoringService } = req.app.locals
|
||||
|
||||
if (!scoringService) {
|
||||
throw new ApiError(400, 'Intelligent verb scoring is not enabled')
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
throw new ApiError(400, 'Learning data is required')
|
||||
}
|
||||
|
||||
const result = await scoringService.importLearningData(data)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: result.message,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
},
|
||||
|
||||
async clearStats(req, res, next) {
|
||||
try {
|
||||
const { scoringService } = req.app.locals
|
||||
|
||||
if (!scoringService) {
|
||||
throw new ApiError(400, 'Intelligent verb scoring is not enabled')
|
||||
}
|
||||
|
||||
const result = await scoringService.clearStats()
|
||||
|
||||
res.json({
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
},
|
||||
|
||||
async analyzePattern(req, res, next) {
|
||||
try {
|
||||
const { sourceType, targetType, relationshipType } = req.body
|
||||
const { scoringService } = req.app.locals
|
||||
|
||||
if (!scoringService) {
|
||||
throw new ApiError(400, 'Intelligent verb scoring is not enabled')
|
||||
}
|
||||
|
||||
if (!sourceType || !targetType || !relationshipType) {
|
||||
throw new ApiError(400, 'sourceType, targetType, and relationshipType are required')
|
||||
}
|
||||
|
||||
const analysis = await scoringService.analyzeRelationshipPattern(
|
||||
sourceType,
|
||||
targetType,
|
||||
relationshipType
|
||||
)
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: analysis
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
},
|
||||
|
||||
async getRecommendations(req, res, next) {
|
||||
try {
|
||||
const { entityId } = req.params
|
||||
const { limit = 10 } = req.query
|
||||
const { scoringService } = req.app.locals
|
||||
|
||||
if (!scoringService) {
|
||||
throw new ApiError(400, 'Intelligent verb scoring is not enabled')
|
||||
}
|
||||
|
||||
const recommendations = await scoringService.getRecommendations(entityId, parseInt(limit))
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: recommendations
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
136
examples/brainy-service-template/src/middleware/errorHandler.js
Normal file
136
examples/brainy-service-template/src/middleware/errorHandler.js
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import { logger } from '../utils/logger.js'
|
||||
import { ApiError } from '../utils/errors.js'
|
||||
|
||||
export const errorHandler = (error, req, res, next) => {
|
||||
// Default error response
|
||||
let statusCode = 500
|
||||
let message = 'Internal Server Error'
|
||||
let details = null
|
||||
|
||||
// Handle known API errors
|
||||
if (error instanceof ApiError) {
|
||||
statusCode = error.statusCode
|
||||
message = error.message
|
||||
|
||||
// Add additional details for validation errors
|
||||
if (error.field) {
|
||||
details = { field: error.field }
|
||||
}
|
||||
|
||||
if (error.resource) {
|
||||
details = {
|
||||
resource: error.resource,
|
||||
resourceId: error.resourceId
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle Brainy-specific errors
|
||||
else if (error.name === 'BrainyError') {
|
||||
statusCode = 400
|
||||
message = error.message
|
||||
details = { type: 'brainy_error' }
|
||||
}
|
||||
// Handle validation errors from other sources
|
||||
else if (error.name === 'ValidationError' || error.name === 'CastError') {
|
||||
statusCode = 400
|
||||
message = error.message
|
||||
details = { type: 'validation_error' }
|
||||
}
|
||||
// Handle JSON parsing errors
|
||||
else if (error instanceof SyntaxError && error.status === 400 && 'body' in error) {
|
||||
statusCode = 400
|
||||
message = 'Invalid JSON format'
|
||||
details = { type: 'json_error' }
|
||||
}
|
||||
// Handle timeout errors
|
||||
else if (error.code === 'ETIMEDOUT' || error.message.includes('timeout')) {
|
||||
statusCode = 504
|
||||
message = 'Request timeout'
|
||||
details = { type: 'timeout_error' }
|
||||
}
|
||||
// Handle rate limiting
|
||||
else if (error.status === 429) {
|
||||
statusCode = 429
|
||||
message = 'Too Many Requests'
|
||||
details = { type: 'rate_limit_error' }
|
||||
}
|
||||
|
||||
// Log the error
|
||||
const logLevel = statusCode >= 500 ? 'error' : 'warn'
|
||||
const logMessage = `${req.method} ${req.originalUrl} - ${statusCode} ${message}`
|
||||
const logMeta = {
|
||||
statusCode,
|
||||
method: req.method,
|
||||
url: req.originalUrl,
|
||||
userAgent: req.get('User-Agent'),
|
||||
ip: req.ip,
|
||||
stack: error.stack
|
||||
}
|
||||
|
||||
logger[logLevel](logMessage, logMeta)
|
||||
|
||||
// Prepare error response
|
||||
const errorResponse = {
|
||||
success: false,
|
||||
error: {
|
||||
message,
|
||||
statusCode,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: req.originalUrl,
|
||||
method: req.method
|
||||
}
|
||||
}
|
||||
|
||||
// Add details if available
|
||||
if (details) {
|
||||
errorResponse.error.details = details
|
||||
}
|
||||
|
||||
// Add stack trace in development
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
errorResponse.error.stack = error.stack
|
||||
}
|
||||
|
||||
// Add request ID if available
|
||||
if (req.id) {
|
||||
errorResponse.error.requestId = req.id
|
||||
}
|
||||
|
||||
res.status(statusCode).json(errorResponse)
|
||||
}
|
||||
|
||||
// 404 handler for unmatched routes
|
||||
export const notFoundHandler = (req, res) => {
|
||||
const message = `Route ${req.method} ${req.originalUrl} not found`
|
||||
|
||||
logger.warn('Route not found', {
|
||||
method: req.method,
|
||||
url: req.originalUrl,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('User-Agent')
|
||||
})
|
||||
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: {
|
||||
message,
|
||||
statusCode: 404,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: req.originalUrl,
|
||||
method: req.method
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Async error wrapper
|
||||
export const asyncHandler = (fn) => {
|
||||
return (req, res, next) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
errorHandler,
|
||||
notFoundHandler,
|
||||
asyncHandler
|
||||
}
|
||||
177
examples/brainy-service-template/src/middleware/validation.js
Normal file
177
examples/brainy-service-template/src/middleware/validation.js
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import { ValidationError } from '../utils/errors.js'
|
||||
|
||||
// Validate required fields
|
||||
export const validateRequired = (fields) => {
|
||||
return (req, res, next) => {
|
||||
const missingFields = []
|
||||
|
||||
fields.forEach(field => {
|
||||
if (req.body[field] === undefined || req.body[field] === null || req.body[field] === '') {
|
||||
missingFields.push(field)
|
||||
}
|
||||
})
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
throw new ValidationError(`Missing required fields: ${missingFields.join(', ')}`)
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
// Validate entity creation
|
||||
export const validateEntity = (req, res, next) => {
|
||||
const { data } = req.body
|
||||
|
||||
if (!data) {
|
||||
throw new ValidationError('Entity data is required')
|
||||
}
|
||||
|
||||
if (typeof data !== 'object' && typeof data !== 'string') {
|
||||
throw new ValidationError('Entity data must be an object or string')
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
// Validate relationship creation
|
||||
export const validateRelationship = (req, res, next) => {
|
||||
const { sourceId, targetId, type, weight } = req.body
|
||||
|
||||
if (!sourceId || !targetId || !type) {
|
||||
throw new ValidationError('sourceId, targetId, and type are required')
|
||||
}
|
||||
|
||||
if (typeof sourceId !== 'string' || typeof targetId !== 'string' || typeof type !== 'string') {
|
||||
throw new ValidationError('sourceId, targetId, and type must be strings')
|
||||
}
|
||||
|
||||
if (sourceId === targetId) {
|
||||
throw new ValidationError('sourceId and targetId cannot be the same')
|
||||
}
|
||||
|
||||
if (weight !== undefined) {
|
||||
if (typeof weight !== 'number' || weight < 0 || weight > 1) {
|
||||
throw new ValidationError('Weight must be a number between 0 and 1')
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
// Validate search query
|
||||
export const validateSearch = (req, res, next) => {
|
||||
const { query, limit, threshold } = req.body
|
||||
|
||||
if (!query) {
|
||||
throw new ValidationError('Search query is required')
|
||||
}
|
||||
|
||||
if (typeof query !== 'string' || query.trim().length === 0) {
|
||||
throw new ValidationError('Search query must be a non-empty string')
|
||||
}
|
||||
|
||||
if (limit !== undefined) {
|
||||
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
||||
throw new ValidationError('Limit must be an integer between 1 and 100')
|
||||
}
|
||||
}
|
||||
|
||||
if (threshold !== undefined) {
|
||||
if (typeof threshold !== 'number' || threshold < 0 || threshold > 1) {
|
||||
throw new ValidationError('Threshold must be a number between 0 and 1')
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
// Validate feedback data
|
||||
export const validateFeedback = (req, res, next) => {
|
||||
const { weight, confidence, type } = req.body
|
||||
|
||||
if (weight === undefined) {
|
||||
throw new ValidationError('Weight is required for feedback')
|
||||
}
|
||||
|
||||
if (typeof weight !== 'number' || weight < 0 || weight > 1) {
|
||||
throw new ValidationError('Weight must be a number between 0 and 1')
|
||||
}
|
||||
|
||||
if (confidence !== undefined) {
|
||||
if (typeof confidence !== 'number' || confidence < 0 || confidence > 1) {
|
||||
throw new ValidationError('Confidence must be a number between 0 and 1')
|
||||
}
|
||||
}
|
||||
|
||||
if (type !== undefined) {
|
||||
const validTypes = ['correction', 'reinforcement', 'adjustment']
|
||||
if (!validTypes.includes(type)) {
|
||||
throw new ValidationError(`Type must be one of: ${validTypes.join(', ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
// Validate pagination parameters
|
||||
export const validatePagination = (req, res, next) => {
|
||||
const { page = 1, limit = 50 } = req.query
|
||||
|
||||
const pageNum = parseInt(page)
|
||||
const limitNum = parseInt(limit)
|
||||
|
||||
if (!Number.isInteger(pageNum) || pageNum < 1) {
|
||||
throw new ValidationError('Page must be a positive integer')
|
||||
}
|
||||
|
||||
if (!Number.isInteger(limitNum) || limitNum < 1 || limitNum > 100) {
|
||||
throw new ValidationError('Limit must be an integer between 1 and 100')
|
||||
}
|
||||
|
||||
// Normalize values
|
||||
req.query.page = pageNum
|
||||
req.query.limit = limitNum
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
// Validate UUID format (basic check)
|
||||
export const validateId = (paramName = 'id') => {
|
||||
return (req, res, next) => {
|
||||
const id = req.params[paramName]
|
||||
|
||||
if (!id) {
|
||||
throw new ValidationError(`${paramName} is required`)
|
||||
}
|
||||
|
||||
// Basic string validation - Brainy uses various ID formats
|
||||
if (typeof id !== 'string' || id.trim().length === 0) {
|
||||
throw new ValidationError(`${paramName} must be a valid string`)
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
// Generic validation wrapper
|
||||
export const validate = (validationFn) => {
|
||||
return (req, res, next) => {
|
||||
try {
|
||||
validationFn(req, res, next)
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
validateRequired,
|
||||
validateEntity,
|
||||
validateRelationship,
|
||||
validateSearch,
|
||||
validateFeedback,
|
||||
validatePagination,
|
||||
validateId,
|
||||
validate
|
||||
}
|
||||
324
examples/brainy-service-template/src/services/brainyService.js
Normal file
324
examples/brainy-service-template/src/services/brainyService.js
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
import { BrainyData } from '@soulcraft/brainy'
|
||||
import config from 'config'
|
||||
import { logger } from '../utils/logger.js'
|
||||
|
||||
export class BrainyService {
|
||||
constructor() {
|
||||
this.db = null
|
||||
this.isInitialized = false
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
if (this.isInitialized) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Build Brainy configuration from app config
|
||||
const brainyConfig = this.buildBrainyConfig()
|
||||
|
||||
// Initialize Brainy database
|
||||
this.db = new BrainyData(brainyConfig)
|
||||
await this.db.init()
|
||||
|
||||
this.isInitialized = true
|
||||
logger.info('Brainy database initialized successfully', {
|
||||
storage: config.get('brainy.storage.type'),
|
||||
features: config.get('brainy.features')
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize Brainy database:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
buildBrainyConfig() {
|
||||
const brainyConfig = {}
|
||||
|
||||
// Storage configuration
|
||||
const storageConfig = config.get('brainy.storage')
|
||||
if (storageConfig.type === 's3' && storageConfig.s3) {
|
||||
brainyConfig.storage = {
|
||||
s3Storage: {
|
||||
bucketName: storageConfig.s3.bucketName,
|
||||
region: storageConfig.s3.region,
|
||||
accessKeyId: storageConfig.s3.accessKeyId,
|
||||
secretAccessKey: storageConfig.s3.secretAccessKey
|
||||
}
|
||||
}
|
||||
} else if (storageConfig.type === 'filesystem') {
|
||||
brainyConfig.storage = {
|
||||
forceFileSystemStorage: true
|
||||
}
|
||||
} else {
|
||||
brainyConfig.storage = {
|
||||
forceMemoryStorage: true
|
||||
}
|
||||
}
|
||||
|
||||
// Cache configuration
|
||||
if (config.has('brainy.cache')) {
|
||||
brainyConfig.cache = config.get('brainy.cache')
|
||||
}
|
||||
|
||||
// Logging configuration
|
||||
if (config.has('brainy.logging')) {
|
||||
brainyConfig.logging = config.get('brainy.logging')
|
||||
}
|
||||
|
||||
// Intelligent verb scoring configuration
|
||||
if (config.get('brainy.features.intelligentVerbScoring')) {
|
||||
brainyConfig.intelligentVerbScoring = config.get('brainy.intelligentVerbScoring')
|
||||
}
|
||||
|
||||
// Real-time updates
|
||||
if (config.get('brainy.features.realTimeUpdates')) {
|
||||
brainyConfig.realtimeUpdates = {
|
||||
enabled: true,
|
||||
interval: 30000,
|
||||
updateStatistics: true,
|
||||
updateIndex: true
|
||||
}
|
||||
}
|
||||
|
||||
// Distributed mode
|
||||
if (config.get('brainy.features.distributedMode')) {
|
||||
brainyConfig.distributed = true
|
||||
}
|
||||
|
||||
return brainyConfig
|
||||
}
|
||||
|
||||
// Entity operations
|
||||
async addEntity(data, metadata = {}, options = {}) {
|
||||
this.ensureInitialized()
|
||||
|
||||
try {
|
||||
const id = await this.db.add(data, metadata, options)
|
||||
logger.debug('Entity added', { id, metadata: metadata.type || 'unknown' })
|
||||
return id
|
||||
} catch (error) {
|
||||
logger.error('Failed to add entity:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async getEntity(id) {
|
||||
this.ensureInitialized()
|
||||
|
||||
try {
|
||||
const entity = await this.db.get(id)
|
||||
return entity
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get entity ${id}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async updateEntity(id, data, metadata = {}) {
|
||||
this.ensureInitialized()
|
||||
|
||||
try {
|
||||
await this.db.update(id, data, metadata)
|
||||
logger.debug('Entity updated', { id })
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.error(`Failed to update entity ${id}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async deleteEntity(id) {
|
||||
this.ensureInitialized()
|
||||
|
||||
try {
|
||||
await this.db.delete(id)
|
||||
logger.debug('Entity deleted', { id })
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.error(`Failed to delete entity ${id}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async searchEntities(query, options = {}) {
|
||||
this.ensureInitialized()
|
||||
|
||||
try {
|
||||
const results = await this.db.search(query, options.limit || 10, {
|
||||
threshold: options.threshold || 0.7,
|
||||
includeMetadata: true,
|
||||
...options
|
||||
})
|
||||
|
||||
logger.debug('Entity search completed', {
|
||||
query: query.substring(0, 50),
|
||||
resultCount: results.length
|
||||
})
|
||||
|
||||
return results
|
||||
} catch (error) {
|
||||
logger.error('Entity search failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async listEntities(options = {}) {
|
||||
this.ensureInitialized()
|
||||
|
||||
try {
|
||||
// Get all entities with pagination if supported
|
||||
const entities = await this.db.getAllNouns(options)
|
||||
return entities
|
||||
} catch (error) {
|
||||
logger.error('Failed to list entities:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Relationship operations
|
||||
async addRelationship(sourceId, targetId, type, options = {}) {
|
||||
this.ensureInitialized()
|
||||
|
||||
try {
|
||||
const relationshipId = await this.db.addVerb(sourceId, targetId, undefined, {
|
||||
type,
|
||||
weight: options.weight,
|
||||
metadata: options.metadata,
|
||||
autoCreateMissingNouns: options.autoCreateMissingNouns || false
|
||||
})
|
||||
|
||||
logger.debug('Relationship added', {
|
||||
relationshipId,
|
||||
sourceId,
|
||||
targetId,
|
||||
type
|
||||
})
|
||||
|
||||
return relationshipId
|
||||
} catch (error) {
|
||||
logger.error('Failed to add relationship:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async getRelationship(id) {
|
||||
this.ensureInitialized()
|
||||
|
||||
try {
|
||||
const relationship = await this.db.getVerb(id)
|
||||
return relationship
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get relationship ${id}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async updateRelationship(id, updates) {
|
||||
this.ensureInitialized()
|
||||
|
||||
try {
|
||||
await this.db.updateVerb(id, updates)
|
||||
logger.debug('Relationship updated', { id })
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.error(`Failed to update relationship ${id}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async deleteRelationship(id) {
|
||||
this.ensureInitialized()
|
||||
|
||||
try {
|
||||
await this.db.deleteVerb(id)
|
||||
logger.debug('Relationship deleted', { id })
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.error(`Failed to delete relationship ${id}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async listRelationships(options = {}) {
|
||||
this.ensureInitialized()
|
||||
|
||||
try {
|
||||
const relationships = await this.db.getVerbs(options)
|
||||
return relationships
|
||||
} catch (error) {
|
||||
logger.error('Failed to list relationships:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Health and metrics
|
||||
async getHealth() {
|
||||
if (!this.isInitialized) {
|
||||
return { status: 'not_initialized' }
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await this.db.getStatistics()
|
||||
return {
|
||||
status: 'healthy',
|
||||
database: {
|
||||
entities: stats.nounCount || 0,
|
||||
relationships: stats.verbCount || 0,
|
||||
indexSize: stats.hnswIndexSize || 0
|
||||
},
|
||||
features: {
|
||||
intelligentVerbScoring: config.get('brainy.features.intelligentVerbScoring'),
|
||||
realTimeUpdates: config.get('brainy.features.realTimeUpdates'),
|
||||
distributedMode: config.get('brainy.features.distributedMode')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Health check failed:', error)
|
||||
return {
|
||||
status: 'unhealthy',
|
||||
error: error.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getMetrics() {
|
||||
this.ensureInitialized()
|
||||
|
||||
try {
|
||||
const stats = await this.db.getStatistics()
|
||||
return {
|
||||
database: stats,
|
||||
performance: {
|
||||
// Add performance metrics here
|
||||
uptime: process.uptime(),
|
||||
memory: process.memoryUsage()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to get metrics:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
ensureInitialized() {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error('BrainyService not initialized')
|
||||
}
|
||||
}
|
||||
|
||||
async shutdown() {
|
||||
if (this.db && this.db.cleanup) {
|
||||
await this.db.cleanup()
|
||||
}
|
||||
this.isInitialized = false
|
||||
logger.info('BrainyService shutdown complete')
|
||||
}
|
||||
|
||||
// Expose the underlying db for advanced operations
|
||||
getDatabase() {
|
||||
this.ensureInitialized()
|
||||
return this.db
|
||||
}
|
||||
}
|
||||
192
examples/brainy-service-template/src/services/scoringService.js
Normal file
192
examples/brainy-service-template/src/services/scoringService.js
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
import { logger } from '../utils/logger.js'
|
||||
|
||||
export class ScoringService {
|
||||
constructor(brainyService) {
|
||||
this.brainyService = brainyService
|
||||
}
|
||||
|
||||
async provideFeedback(relationshipId, feedbackData) {
|
||||
try {
|
||||
// Get the relationship to extract source, target, and type
|
||||
const relationship = await this.brainyService.getRelationship(relationshipId)
|
||||
|
||||
if (!relationship) {
|
||||
throw new Error(`Relationship ${relationshipId} not found`)
|
||||
}
|
||||
|
||||
// Provide feedback to the intelligent scoring system
|
||||
const db = this.brainyService.getDatabase()
|
||||
await db.provideFeedbackForVerbScoring(
|
||||
relationship.sourceId,
|
||||
relationship.targetId,
|
||||
relationship.type || relationship.verb,
|
||||
feedbackData.weight,
|
||||
feedbackData.confidence,
|
||||
feedbackData.type || 'correction'
|
||||
)
|
||||
|
||||
logger.info('Feedback provided for relationship', {
|
||||
relationshipId,
|
||||
feedbackType: feedbackData.type || 'correction',
|
||||
weight: feedbackData.weight,
|
||||
confidence: feedbackData.confidence
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Feedback provided successfully'
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to provide feedback:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async getStats() {
|
||||
try {
|
||||
const db = this.brainyService.getDatabase()
|
||||
const stats = db.getVerbScoringStats()
|
||||
|
||||
if (!stats) {
|
||||
return {
|
||||
message: 'Intelligent verb scoring is not enabled or has no data yet',
|
||||
stats: null
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug('Retrieved scoring statistics', {
|
||||
totalRelationships: stats.totalRelationships
|
||||
})
|
||||
|
||||
return {
|
||||
stats,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to get scoring statistics:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async exportLearningData() {
|
||||
try {
|
||||
const db = this.brainyService.getDatabase()
|
||||
const learningData = db.exportVerbScoringLearningData()
|
||||
|
||||
if (!learningData) {
|
||||
return {
|
||||
message: 'No learning data available for export',
|
||||
data: null
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Learning data exported successfully')
|
||||
|
||||
return {
|
||||
data: learningData,
|
||||
timestamp: new Date().toISOString(),
|
||||
format: 'json'
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to export learning data:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async importLearningData(learningData) {
|
||||
try {
|
||||
const db = this.brainyService.getDatabase()
|
||||
|
||||
// If data is an object, stringify it
|
||||
const dataString = typeof learningData === 'string'
|
||||
? learningData
|
||||
: JSON.stringify(learningData)
|
||||
|
||||
db.importVerbScoringLearningData(dataString)
|
||||
|
||||
logger.info('Learning data imported successfully')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Learning data imported successfully'
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to import learning data:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async clearStats() {
|
||||
try {
|
||||
const db = this.brainyService.getDatabase()
|
||||
|
||||
// Access the intelligent scoring system directly if available
|
||||
if (db.intelligentVerbScoring && db.intelligentVerbScoring.enabled) {
|
||||
db.intelligentVerbScoring.clearStats()
|
||||
|
||||
logger.info('Scoring statistics cleared')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Scoring statistics cleared successfully'
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Intelligent verb scoring is not enabled'
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to clear scoring statistics:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Analysis methods
|
||||
async analyzeRelationshipPattern(sourceType, targetType, relationshipType) {
|
||||
try {
|
||||
const stats = await this.getStats()
|
||||
|
||||
if (!stats.stats) {
|
||||
return {
|
||||
message: 'No data available for pattern analysis'
|
||||
}
|
||||
}
|
||||
|
||||
// Find patterns for the specific relationship type
|
||||
const pattern = `${sourceType}-${relationshipType}-${targetType}`
|
||||
const relevantRelationships = stats.stats.topRelationships.filter(rel =>
|
||||
rel.relationship.includes(relationshipType)
|
||||
)
|
||||
|
||||
return {
|
||||
pattern,
|
||||
relationships: relevantRelationships,
|
||||
analysis: {
|
||||
averageWeight: relevantRelationships.reduce((sum, rel) => sum + rel.averageWeight, 0) / relevantRelationships.length || 0,
|
||||
totalOccurrences: relevantRelationships.reduce((sum, rel) => sum + rel.count, 0),
|
||||
confidence: relevantRelationships.length > 5 ? 'high' : 'low'
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to analyze relationship pattern:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async getRecommendations(entityId, limit = 10) {
|
||||
try {
|
||||
// This would implement recommendation logic based on learned patterns
|
||||
// For now, return a placeholder
|
||||
return {
|
||||
entityId,
|
||||
recommendations: [],
|
||||
message: 'Recommendation system not yet implemented',
|
||||
basedOn: 'intelligent_verb_scoring_patterns'
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to get recommendations:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
95
examples/brainy-service-template/src/utils/errors.js
Normal file
95
examples/brainy-service-template/src/utils/errors.js
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
export class ApiError extends Error {
|
||||
constructor(statusCode, message, isOperational = true, stack = '') {
|
||||
super(message)
|
||||
this.name = this.constructor.name
|
||||
this.statusCode = statusCode
|
||||
this.isOperational = isOperational
|
||||
|
||||
if (stack) {
|
||||
this.stack = stack
|
||||
} else {
|
||||
Error.captureStackTrace(this, this.constructor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidationError extends ApiError {
|
||||
constructor(message, field = null) {
|
||||
super(400, message)
|
||||
this.field = field
|
||||
this.name = 'ValidationError'
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends ApiError {
|
||||
constructor(resource, id = null) {
|
||||
const message = id
|
||||
? `${resource} with ID ${id} not found`
|
||||
: `${resource} not found`
|
||||
super(404, message)
|
||||
this.name = 'NotFoundError'
|
||||
this.resource = resource
|
||||
this.resourceId = id
|
||||
}
|
||||
}
|
||||
|
||||
export class ConflictError extends ApiError {
|
||||
constructor(message) {
|
||||
super(409, message)
|
||||
this.name = 'ConflictError'
|
||||
}
|
||||
}
|
||||
|
||||
export class DatabaseError extends ApiError {
|
||||
constructor(message, operation = null) {
|
||||
super(500, message)
|
||||
this.name = 'DatabaseError'
|
||||
this.operation = operation
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthenticationError extends ApiError {
|
||||
constructor(message = 'Authentication failed') {
|
||||
super(401, message)
|
||||
this.name = 'AuthenticationError'
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthorizationError extends ApiError {
|
||||
constructor(message = 'Insufficient permissions') {
|
||||
super(403, message)
|
||||
this.name = 'AuthorizationError'
|
||||
}
|
||||
}
|
||||
|
||||
export class RateLimitError extends ApiError {
|
||||
constructor(message = 'Rate limit exceeded') {
|
||||
super(429, message)
|
||||
this.name = 'RateLimitError'
|
||||
}
|
||||
}
|
||||
|
||||
// Error factory for common scenarios
|
||||
export const createError = {
|
||||
validation: (message, field) => new ValidationError(message, field),
|
||||
notFound: (resource, id) => new NotFoundError(resource, id),
|
||||
conflict: (message) => new ConflictError(message),
|
||||
database: (message, operation) => new DatabaseError(message, operation),
|
||||
auth: (message) => new AuthenticationError(message),
|
||||
forbidden: (message) => new AuthorizationError(message),
|
||||
rateLimit: (message) => new RateLimitError(message),
|
||||
badRequest: (message) => new ApiError(400, message),
|
||||
internal: (message) => new ApiError(500, message)
|
||||
}
|
||||
|
||||
export default {
|
||||
ApiError,
|
||||
ValidationError,
|
||||
NotFoundError,
|
||||
ConflictError,
|
||||
DatabaseError,
|
||||
AuthenticationError,
|
||||
AuthorizationError,
|
||||
RateLimitError,
|
||||
createError
|
||||
}
|
||||
63
examples/brainy-service-template/src/utils/logger.js
Normal file
63
examples/brainy-service-template/src/utils/logger.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import winston from 'winston'
|
||||
import config from 'config'
|
||||
|
||||
const logLevel = config.get('logging.level') || 'info'
|
||||
const logFormat = config.get('logging.format') || 'combined'
|
||||
|
||||
// Create custom format for development
|
||||
const devFormat = winston.format.combine(
|
||||
winston.format.timestamp({ format: 'HH:mm:ss' }),
|
||||
winston.format.colorize(),
|
||||
winston.format.printf(({ timestamp, level, message, ...meta }) => {
|
||||
const metaStr = Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''
|
||||
return `${timestamp} [${level}]: ${message} ${metaStr}`
|
||||
})
|
||||
)
|
||||
|
||||
// Create custom format for production
|
||||
const prodFormat = winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.json()
|
||||
)
|
||||
|
||||
// Create logger instance
|
||||
export const logger = winston.createLogger({
|
||||
level: logLevel,
|
||||
format: logFormat === 'dev' ? devFormat : prodFormat,
|
||||
defaultMeta: {
|
||||
service: 'brainy-service',
|
||||
version: process.env.npm_package_version || '1.0.0'
|
||||
},
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
handleExceptions: true,
|
||||
handleRejections: true
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
// Add file logging for production
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
logger.add(new winston.transports.File({
|
||||
filename: 'logs/error.log',
|
||||
level: 'error',
|
||||
handleExceptions: true,
|
||||
maxsize: 5242880, // 5MB
|
||||
maxFiles: 5
|
||||
}))
|
||||
|
||||
logger.add(new winston.transports.File({
|
||||
filename: 'logs/combined.log',
|
||||
maxsize: 5242880, // 5MB
|
||||
maxFiles: 5
|
||||
}))
|
||||
}
|
||||
|
||||
// Capture unhandled errors
|
||||
logger.exceptions.handle(
|
||||
new winston.transports.Console(),
|
||||
new winston.transports.File({ filename: 'logs/exceptions.log' })
|
||||
)
|
||||
|
||||
export default logger
|
||||
135
examples/brainy-service-template/tests/entities.test.js
Normal file
135
examples/brainy-service-template/tests/entities.test.js
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'
|
||||
import request from 'supertest'
|
||||
import app from '../src/app.js'
|
||||
|
||||
describe('Entity Endpoints', () => {
|
||||
let server
|
||||
let testEntityId
|
||||
|
||||
beforeAll(async () => {
|
||||
server = app.listen(0)
|
||||
// Wait for Brainy to initialize
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (server) {
|
||||
server.close()
|
||||
}
|
||||
})
|
||||
|
||||
describe('POST /api/entities', () => {
|
||||
it('should create a new entity', async () => {
|
||||
const entityData = {
|
||||
data: { name: 'Test Entity', type: 'test' },
|
||||
metadata: { category: 'test' }
|
||||
}
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/entities')
|
||||
.send(entityData)
|
||||
.expect(201)
|
||||
.expect('Content-Type', /json/)
|
||||
|
||||
expect(response.body).toHaveProperty('success', true)
|
||||
expect(response.body).toHaveProperty('id')
|
||||
expect(response.body.data).toHaveProperty('data')
|
||||
expect(response.body.data.data).toEqual(entityData.data)
|
||||
|
||||
testEntityId = response.body.id
|
||||
})
|
||||
|
||||
it('should return 400 for missing data', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/entities')
|
||||
.send({})
|
||||
.expect(400)
|
||||
.expect('Content-Type', /json/)
|
||||
|
||||
expect(response.body).toHaveProperty('success', false)
|
||||
expect(response.body.error).toHaveProperty('message')
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/entities/:id', () => {
|
||||
it('should retrieve an entity by ID', async () => {
|
||||
if (!testEntityId) {
|
||||
// Create test entity first
|
||||
const entityData = {
|
||||
data: { name: 'Test Entity', type: 'test' }
|
||||
}
|
||||
const createResponse = await request(app)
|
||||
.post('/api/entities')
|
||||
.send(entityData)
|
||||
testEntityId = createResponse.body.id
|
||||
}
|
||||
|
||||
const response = await request(app)
|
||||
.get(`/api/entities/${testEntityId}`)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
|
||||
expect(response.body).toHaveProperty('success', true)
|
||||
expect(response.body).toHaveProperty('data')
|
||||
expect(response.body.data).toHaveProperty('id', testEntityId)
|
||||
})
|
||||
|
||||
it('should return 404 for non-existent entity', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/entities/non-existent-id')
|
||||
.expect(404)
|
||||
.expect('Content-Type', /json/)
|
||||
|
||||
expect(response.body).toHaveProperty('success', false)
|
||||
expect(response.body.error).toHaveProperty('statusCode', 404)
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/entities/search', () => {
|
||||
it('should search entities', async () => {
|
||||
const searchQuery = {
|
||||
query: 'test entity',
|
||||
limit: 5,
|
||||
threshold: 0.5
|
||||
}
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/entities/search')
|
||||
.send(searchQuery)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
|
||||
expect(response.body).toHaveProperty('success', true)
|
||||
expect(response.body).toHaveProperty('results')
|
||||
expect(response.body).toHaveProperty('query', searchQuery.query)
|
||||
expect(Array.isArray(response.body.results)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return 400 for missing query', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/entities/search')
|
||||
.send({})
|
||||
.expect(400)
|
||||
.expect('Content-Type', /json/)
|
||||
|
||||
expect(response.body).toHaveProperty('success', false)
|
||||
expect(response.body.error.message).toContain('Query is required')
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/entities', () => {
|
||||
it('should list entities with pagination', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/entities?page=1&limit=10')
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
|
||||
expect(response.body).toHaveProperty('success', true)
|
||||
expect(response.body).toHaveProperty('data')
|
||||
expect(response.body).toHaveProperty('pagination')
|
||||
expect(response.body.pagination).toHaveProperty('page', 1)
|
||||
expect(response.body.pagination).toHaveProperty('limit', 10)
|
||||
expect(Array.isArray(response.body.data)).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
75
examples/brainy-service-template/tests/health.test.js
Normal file
75
examples/brainy-service-template/tests/health.test.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
||||
import request from 'supertest'
|
||||
import app from '../src/app.js'
|
||||
|
||||
describe('Health Endpoints', () => {
|
||||
let server
|
||||
|
||||
beforeAll(async () => {
|
||||
server = app.listen(0) // Use random available port
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (server) {
|
||||
server.close()
|
||||
}
|
||||
})
|
||||
|
||||
describe('GET /health', () => {
|
||||
it('should return health status', async () => {
|
||||
const response = await request(app)
|
||||
.get('/health')
|
||||
.expect('Content-Type', /json/)
|
||||
|
||||
expect(response.status).toBeGreaterThanOrEqual(200)
|
||||
expect(response.body).toHaveProperty('status')
|
||||
expect(response.body).toHaveProperty('timestamp')
|
||||
expect(response.body).toHaveProperty('version')
|
||||
expect(response.body).toHaveProperty('services')
|
||||
expect(response.body.services).toHaveProperty('brainy')
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /health/liveness', () => {
|
||||
it('should return liveness status', async () => {
|
||||
const response = await request(app)
|
||||
.get('/health/liveness')
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
|
||||
expect(response.body).toHaveProperty('alive', true)
|
||||
expect(response.body).toHaveProperty('timestamp')
|
||||
expect(response.body).toHaveProperty('pid')
|
||||
expect(response.body).toHaveProperty('uptime')
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /health/readiness', () => {
|
||||
it('should return readiness status', async () => {
|
||||
const response = await request(app)
|
||||
.get('/health/readiness')
|
||||
.expect('Content-Type', /json/)
|
||||
|
||||
expect(response.status).toBeGreaterThanOrEqual(200)
|
||||
expect(response.body).toHaveProperty('ready')
|
||||
expect(response.body).toHaveProperty('timestamp')
|
||||
expect(response.body).toHaveProperty('checks')
|
||||
expect(response.body.checks).toHaveProperty('brainy')
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /health/metrics', () => {
|
||||
it('should return system metrics', async () => {
|
||||
const response = await request(app)
|
||||
.get('/health/metrics')
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
|
||||
expect(response.body).toHaveProperty('success', true)
|
||||
expect(response.body).toHaveProperty('data')
|
||||
expect(response.body.data).toHaveProperty('timestamp')
|
||||
expect(response.body.data).toHaveProperty('system')
|
||||
expect(response.body.data.system).toHaveProperty('process')
|
||||
})
|
||||
})
|
||||
})
|
||||
33
examples/brainy-service-template/tests/setup.js
Normal file
33
examples/brainy-service-template/tests/setup.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { beforeAll, afterAll } from 'vitest'
|
||||
import config from 'config'
|
||||
|
||||
// Global test setup
|
||||
beforeAll(async () => {
|
||||
// Set test environment
|
||||
process.env.NODE_ENV = 'test'
|
||||
process.env.LOG_LEVEL = 'error'
|
||||
|
||||
// Suppress console output during tests
|
||||
const originalConsoleLog = console.log
|
||||
const originalConsoleWarn = console.warn
|
||||
|
||||
console.log = (...args) => {
|
||||
if (!args[0]?.includes?.('Model loaded') && !args[0]?.includes?.('Brainy')) {
|
||||
originalConsoleLog.apply(console, args)
|
||||
}
|
||||
}
|
||||
|
||||
console.warn = (...args) => {
|
||||
if (!args[0]?.includes?.('Model') && !args[0]?.includes?.('TensorFlow')) {
|
||||
originalConsoleWarn.apply(console, args)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
// Cleanup any global resources
|
||||
// Force garbage collection if available
|
||||
if (global.gc) {
|
||||
global.gc()
|
||||
}
|
||||
})
|
||||
26
examples/brainy-service-template/vitest.config.js
Normal file
26
examples/brainy-service-template/vitest.config.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'node',
|
||||
timeout: 60000, // 60 seconds for model loading
|
||||
testTimeout: 60000,
|
||||
hookTimeout: 60000,
|
||||
teardownTimeout: 60000,
|
||||
reporters: process.env.CI ? ['json'] : ['verbose'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: [
|
||||
'coverage/**',
|
||||
'dist/**',
|
||||
'**/node_modules/**',
|
||||
'**/*.config.js',
|
||||
'**/*.config.ts',
|
||||
'tests/**'
|
||||
]
|
||||
},
|
||||
setupFiles: ['./tests/setup.js'],
|
||||
globalSetup: ['./tests/globalSetup.js']
|
||||
}
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue