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
c38c8de10a
commit
768acf66ad
30 changed files with 3548 additions and 0 deletions
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue