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:
David Snelling 2025-08-06 18:17:32 -07:00
parent 880b8f74e3
commit ee003a9473
30 changed files with 3548 additions and 0 deletions

View 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

View file

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

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

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

View file

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

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

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

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

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

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

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

View 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