/**
* Ragno: Production API Server
*
* This module provides the main API server that integrates all ragno components
* into a production-ready REST API service with comprehensive monitoring,
* caching, and management capabilities.
*/
import express from 'express'
import cors from 'cors'
import helmet from 'helmet'
import rateLimit from 'express-rate-limit'
import compression from 'compression'
import GraphAPI from './GraphAPI.js'
import SearchAPIEnhanced from './SearchAPIEnhanced.js'
import GraphMetrics from '../monitoring/GraphMetrics.js'
import GraphCache from '../cache/GraphCache.js'
import { VectorIndex } from '../search/index.js'
import { logger } from '../../Utils.js'
/**
* Main Ragno API Server with production features
*/
export class RagnoAPIServer {
constructor(options = {}) {
this.options = {
// Server configuration
port: options.port || 3000,
host: options.host || '0.0.0.0',
apiVersion: options.apiVersion || 'v1',
// Core dependencies
llmHandler: options.llmHandler,
embeddingHandler: options.embeddingHandler,
sparqlEndpoint: options.sparqlEndpoint,
// Security
enableCors: options.enableCors !== false,
corsOptions: options.corsOptions || {
origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
credentials: true
},
// Rate limiting
enableRateLimit: options.enableRateLimit !== false,
rateLimitOptions: {
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again later',
...options.rateLimitOptions
},
// Production features
enableMetrics: options.enableMetrics !== false,
enableCaching: options.enableCaching !== false,
enableCompression: options.enableCompression !== false,
enableHelmet: options.enableHelmet !== false,
// Component options
cacheOptions: options.cacheOptions || {},
metricsOptions: options.metricsOptions || {},
vectorIndexPath: options.vectorIndexPath,
// API configuration
enableSwagger: options.enableSwagger !== false,
apiDocumentation: options.apiDocumentation !== false,
...options
}
// Express app
this.app = express()
this.server = null
// Core components
this.cache = null
this.metrics = null
this.vectorIndex = null
this.graphAPI = null
this.searchAPI = null
// Health status
this.healthy = false
this.startTime = null
}
/**
* Initialize the API server
*/
async initialize() {
logger.info('Initializing Ragno API Server...')
this.startTime = Date.now()
try {
// Initialize core components
await this._initializeComponents()
// Setup Express middleware
this._setupMiddleware()
// Setup API routes
this._setupRoutes()
// Setup error handling
this._setupErrorHandling()
this.healthy = true
logger.info('Ragno API Server initialized successfully')
} catch (error) {
logger.error('Failed to initialize Ragno API Server:', error)
throw error
}
}
/**
* Start the server
*/
async start() {
if (!this.healthy) {
throw new Error('Server not initialized')
}
return new Promise((resolve, reject) => {
this.server = this.app.listen(this.options.port, this.options.host, (error) => {
if (error) {
logger.error('Failed to start server:', error)
reject(error)
} else {
logger.info(`Ragno API Server listening on ${this.options.host}:${this.options.port}`)
logger.info(`API documentation available at http://${this.options.host}:${this.options.port}/docs`)
resolve()
}
})
})
}
/**
* Stop the server
*/
async stop() {
logger.info('Stopping Ragno API Server...')
if (this.server) {
await new Promise((resolve) => {
this.server.close(resolve)
})
}
// Cleanup components
await this._cleanupComponents()
this.healthy = false
logger.info('Ragno API Server stopped')
}
/**
* Initialize core components
*/
async _initializeComponents() {
// Initialize cache
if (this.options.enableCaching) {
this.cache = new GraphCache(this.options.cacheOptions)
await this.cache.initialize()
}
// Initialize metrics
if (this.options.enableMetrics) {
this.metrics = new GraphMetrics(this.options.metricsOptions)
await this.metrics.initialize()
}
// Initialize vector index
if (this.options.vectorIndexPath) {
try {
this.vectorIndex = new VectorIndex()
await this.vectorIndex.load(this.options.vectorIndexPath)
logger.info('Vector index loaded successfully')
} catch (error) {
logger.warn('Failed to load vector index:', error.message)
}
}
// Initialize Graph API
this.graphAPI = new GraphAPI({
llmHandler: this.options.llmHandler,
embeddingHandler: this.options.embeddingHandler,
sparqlEndpoint: this.options.sparqlEndpoint,
cache: this.cache,
metrics: this.metrics,
enableCaching: this.options.enableCaching,
enableMetrics: this.options.enableMetrics
})
await this.graphAPI.initialize()
// Initialize Enhanced Search API
this.searchAPI = new SearchAPIEnhanced({
sparqlEndpoint: this.options.sparqlEndpoint,
llmHandler: this.options.llmHandler,
embeddingHandler: this.options.embeddingHandler,
vectorIndex: this.vectorIndex,
cache: this.cache,
metrics: this.metrics,
enableCaching: this.options.enableCaching,
enableMetrics: this.options.enableMetrics
})
await this.searchAPI.initialize()
}
/**
* Setup Express middleware
*/
_setupMiddleware() {
// Security headers
if (this.options.enableHelmet) {
this.app.use(helmet())
}
// CORS
if (this.options.enableCors) {
this.app.use(cors(this.options.corsOptions))
}
// Compression
if (this.options.enableCompression) {
this.app.use(compression())
}
// Rate limiting
if (this.options.enableRateLimit) {
const limiter = rateLimit(this.options.rateLimitOptions)
this.app.use(limiter)
}
// Body parsing
this.app.use(express.json({ limit: '10mb' }))
this.app.use(express.urlencoded({ extended: true, limit: '10mb' }))
// Request logging
this.app.use((req, res, next) => {
const start = Date.now()
res.on('finish', () => {
const duration = Date.now() - start
logger.info(`${req.method} ${req.url} - ${res.statusCode} (${duration}ms)`)
})
next()
})
// Request ID and timing
this.app.use((req, res, next) => {
req.id = `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
req.startTime = Date.now()
res.setHeader('X-Request-ID', req.id)
next()
})
}
/**
* Setup API routes
*/
_setupRoutes() {
const apiPrefix = `/api/${this.options.apiVersion}`
// Health check
this.app.get('/health', this._handleHealthCheck.bind(this))
this.app.get('/ready', this._handleReadinessCheck.bind(this))
// Root endpoint
this.app.get('/', this._handleRoot.bind(this))
// API Info
this.app.get(apiPrefix, this._handleAPIInfo.bind(this))
// Mount Graph API
this.app.use(`${apiPrefix}/graph`, this.graphAPI.getRouter())
// Mount Search API
this.app.use(`${apiPrefix}/search`, this.searchAPI.getRouter())
// System endpoints
this.app.get(`${apiPrefix}/system/info`, this._handleSystemInfo.bind(this))
this.app.get(`${apiPrefix}/system/metrics`, this._handleSystemMetrics.bind(this))
this.app.post(`${apiPrefix}/system/cache/clear`, this._handleClearCache.bind(this))
// API Documentation
if (this.options.enableSwagger) {
this._setupSwagger(apiPrefix)
}
}
/**
* Setup error handling
*/
_setupErrorHandling() {
// 404 handler
this.app.use('*', (req, res) => {
res.status(404).json({
error: 'Not Found',
message: `The requested resource ${req.originalUrl} was not found`,
timestamp: new Date().toISOString()
})
})
// Global error handler
this.app.use((error, req, res, next) => {
logger.error('Unhandled error:', error)
if (this.metrics) {
this.metrics.recordError(error, { requestId: req.id, url: req.url })
}
// Don't expose internal errors in production
const isDevelopment = process.env.NODE_ENV === 'development'
res.status(error.status || 500).json({
error: 'Internal Server Error',
message: isDevelopment ? error.message : 'An unexpected error occurred',
requestId: req.id,
timestamp: new Date().toISOString(),
...(isDevelopment && { stack: error.stack })
})
})
}
/**
* Handle root endpoint
*/
_handleRoot(req, res) {
res.json({
name: 'Ragno Knowledge Graph API',
version: this.options.apiVersion,
description: 'Production-ready REST API for ragno knowledge graph operations',
endpoints: {
health: '/health',
api: `/api/${this.options.apiVersion}`,
docs: this.options.enableSwagger ? '/docs' : null
},
uptime: process.uptime(),
timestamp: new Date().toISOString()
})
}
/**
* Handle API info endpoint
*/
_handleAPIInfo(req, res) {
res.json({
version: this.options.apiVersion,
endpoints: {
graph: {
stats: 'GET /graph/stats',
entities: 'GET /graph/entities',
export: 'GET /graph/export/{format}',
decompose: 'POST /graph/decompose',
pipeline: 'POST /graph/pipeline/full'
},
search: {
unified: 'POST /search/unified',
semantic: 'POST /search/semantic',
entities: 'POST /search/entities',
faceted: 'POST /search/faceted'
},
system: {
info: 'GET /system/info',
metrics: 'GET /system/metrics',
clearCache: 'POST /system/cache/clear'
}
},
capabilities: {
caching: this.options.enableCaching,
metrics: this.options.enableMetrics,
vectorSearch: !!this.vectorIndex,
sparqlIntegration: !!this.options.sparqlEndpoint
}
})
}
/**
* Handle health check
*/
_handleHealthCheck(req, res) {
const health = {
status: this.healthy ? 'healthy' : 'unhealthy',
uptime: process.uptime(),
timestamp: new Date().toISOString(),
version: this.options.apiVersion,
components: {
cache: this.cache ? 'initialized' : 'disabled',
metrics: this.metrics ? 'initialized' : 'disabled',
vectorIndex: this.vectorIndex ? 'loaded' : 'not_loaded',
graphAPI: this.graphAPI ? 'ready' : 'not_ready',
searchAPI: this.searchAPI ? 'ready' : 'not_ready'
}
}
// Add detailed health info if metrics are enabled
if (this.metrics) {
health.systemHealth = this.metrics.getHealthStatus()
}
const statusCode = this.healthy ? 200 : 503
res.status(statusCode).json(health)
}
/**
* Handle readiness check
*/
_handleReadinessCheck(req, res) {
const ready = this.healthy &&
this.graphAPI &&
this.searchAPI &&
(!this.options.sparqlEndpoint || this._checkSPARQLConnection())
res.status(ready ? 200 : 503).json({
status: ready ? 'ready' : 'not_ready',
timestamp: new Date().toISOString()
})
}
/**
* Handle system info
*/
_handleSystemInfo(req, res) {
const info = {
server: {
name: 'Ragno API Server',
version: this.options.apiVersion,
uptime: process.uptime(),
startTime: new Date(this.startTime).toISOString(),
nodeVersion: process.version,
platform: process.platform,
arch: process.arch
},
memory: process.memoryUsage(),
configuration: {
caching: this.options.enableCaching,
metrics: this.options.enableMetrics,
rateLimit: this.options.enableRateLimit,
compression: this.options.enableCompression,
vectorIndex: !!this.vectorIndex,
sparqlEndpoint: !!this.options.sparqlEndpoint
},
components: {
cache: this.cache ? 'enabled' : 'disabled',
metrics: this.metrics ? 'enabled' : 'disabled',
vectorIndex: this.vectorIndex ? 'loaded' : 'not_loaded'
}
}
res.json(info)
}
/**
* Handle system metrics
*/
async _handleSystemMetrics(req, res) {
try {
const metrics = {
timestamp: new Date().toISOString(),
server: {
uptime: process.uptime(),
memory: process.memoryUsage(),
cpu: process.cpuUsage()
}
}
if (this.metrics) {
metrics.application = await this.metrics.getMetricsSummary()
metrics.search = await this.metrics.getSearchMetrics()
}
if (this.cache) {
metrics.cache = await this.cache.getStatistics()
}
if (this.vectorIndex) {
metrics.vectorIndex = await this.vectorIndex.getStatistics()
}
res.json(metrics)
} catch (error) {
logger.error('Failed to get system metrics:', error)
res.status(500).json({
error: 'Failed to retrieve system metrics',
timestamp: new Date().toISOString()
})
}
}
/**
* Handle cache clear
*/
async _handleClearCache(req, res) {
try {
if (!this.cache) {
return res.status(400).json({
error: 'Cache not enabled',
message: 'Caching is not enabled on this server'
})
}
await this.cache.clear()
res.json({
message: 'Cache cleared successfully',
timestamp: new Date().toISOString()
})
} catch (error) {
logger.error('Failed to clear cache:', error)
res.status(500).json({
error: 'Failed to clear cache',
message: error.message
})
}
}
/**
* Setup Swagger documentation
*/
_setupSwagger(apiPrefix) {
try {
// Import swagger modules dynamically
import('swagger-jsdoc').then(swaggerJSDoc => {
import('swagger-ui-express').then(swaggerUi => {
const swaggerOptions = {
definition: {
openapi: '3.0.0',
info: {
title: 'Ragno Knowledge Graph API',
version: this.options.apiVersion,
description: 'Production-ready REST API for ragno knowledge graph operations',
contact: {
name: 'Ragno API Support'
}
},
servers: [{
url: `http://${this.options.host}:${this.options.port}${apiPrefix}`,
description: 'Development server'
}]
},
apis: ['./src/ragno/api/*.js'] // Paths to files containing OpenAPI definitions
}
const swaggerSpec = swaggerJSDoc.default(swaggerOptions)
this.app.use('/docs', swaggerUi.default.serve)
this.app.get('/docs', swaggerUi.default.setup(swaggerSpec))
this.app.get('/docs.json', (req, res) => {
res.json(swaggerSpec)
})
logger.info('Swagger documentation setup complete')
})
})
} catch (error) {
logger.warn('Failed to setup Swagger documentation:', error.message)
}
}
/**
* Check SPARQL connection
*/
async _checkSPARQLConnection() {
if (!this.options.sparqlEndpoint) return true
try {
// Simple SPARQL query to test connection
const { SPARQLHelpers } = await import('../../utils/SPARQLHelpers.js')
await SPARQLHelpers.executeSPARQLQuery(
this.options.sparqlEndpoint,
'SELECT (COUNT(*) as ?count) WHERE { ?s ?p ?o } LIMIT 1'
)
return true
} catch (error) {
logger.warn('SPARQL connection check failed:', error.message)
return false
}
}
/**
* Cleanup components
*/
async _cleanupComponents() {
if (this.searchAPI) {
await this.searchAPI.shutdown()
}
if (this.graphAPI) {
await this.graphAPI.shutdown()
}
if (this.metrics) {
await this.metrics.shutdown()
}
if (this.cache) {
await this.cache.shutdown()
}
}
/**
* Get the Express app instance
*/
getApp() {
return this.app
}
/**
* Get server status
*/
getStatus() {
return {
healthy: this.healthy,
uptime: process.uptime(),
startTime: this.startTime,
components: {
cache: !!this.cache,
metrics: !!this.metrics,
vectorIndex: !!this.vectorIndex,
graphAPI: !!this.graphAPI,
searchAPI: !!this.searchAPI
}
}
}
}
export default RagnoAPIServer