Source: services/search/SearchServer.js

import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';
import logger from 'loglevel';

import SearchService from './SearchService.js';
import EmbeddingService from '../embeddings/EmbeddingService.js';
import SPARQLService from '../embeddings/SPARQLService.js';
import APIRegistry from '../../api/common/APIRegistry.js';
import ChatAPI from '../../api/features/ChatAPI.js';
import MemoryManager from '../../MemoryManager.js';
import LLMHandler from '../../handlers/LLMHandler.js';
import EmbeddingConnectorFactory from '../../connectors/EmbeddingConnectorFactory.js';
import Config from '../../Config.js';

/**
 * Search server application
 */
class SearchServer {
    /**
     * Creates a new SearchServer
     * @param {Object} options - Configuration options
     * @param {number} options.port - The port to listen on
     * @param {string} options.graphName - The graph name to search in
     */
    constructor(options = {}) {
        this.port = options.port || 4100;
        this.graphName = options.graphName || 'http://hyperdata.it/content';
        this.chatModel = options.chatModel || 'qwen2:1.5b';
        this.embeddingModel = options.embeddingModel || 'nomic-embed-text';

        // Initialize services
        this.embeddingService = new EmbeddingService();
        this.sparqlService = new SPARQLService({
            queryEndpoint: 'http://localhost:4030/semem/query',
            updateEndpoint: 'http://localhost:4030/semem/update',
            graphName: this.graphName,
            auth: {
                user: 'admin',
                password: 'admin123'
            }
        });

        this.searchService = new SearchService({
            embeddingService: this.embeddingService,
            sparqlService: this.sparqlService,
            graphName: this.graphName
        });

        // Initialize API registry
        this.apiRegistry = new APIRegistry();

        // Create Express app
        this.app = express();

        // Get directory name for ES modules
        const __filename = fileURLToPath(import.meta.url);
        const __dirname = path.dirname(__filename);

        // Calculate paths for project root and public directory
        this.projectRoot = path.resolve(__dirname, '..', '..', '..');
        this.publicDir = path.join(this.projectRoot, 'public');

        logger.info(`SearchServer initialized with port: ${this.port}, graph: ${this.graphName}`);
    }

    /**
     * Configure the Express app
     */
    configureApp() {
        // Middleware
        this.app.use(express.json());
        this.app.use(express.urlencoded({ extended: true }));
        this.app.use(express.static(this.publicDir));

        // API endpoint for searching
        this.app.get('/api/search', this.handleSearch.bind(this));

        // HTML route for the search form
        this.app.get('/', (req, res) => {
            res.sendFile(path.join(this.publicDir, 'index.html'));
        });
    }

    /**
     * Handle search API requests
     * @param {Request} req - The Express request
     * @param {Response} res - The Express response
     */
    async handleSearch(req, res) {
        try {
            const query = req.query.q || '';
            const limit = parseInt(req.query.limit) || 5;
            const graph = req.query.graph || this.graphName;

            logger.info(`Search request for: "${query}" with limit: ${limit}, graph: ${graph}`);

            if (!query.trim()) {
                return res.json({ results: [] });
            }

            // If the requested graph is different from the current one, create a new search service
            let searchService = this.searchService;
            if (graph !== this.graphName) {
                logger.info(`Using different graph: ${graph} (current: ${this.graphName})`);
                
                // Create a new SPARQLService with the requested graph
                const sparqlService = new SPARQLService({
                    queryEndpoint: 'http://localhost:4030/semem/query',
                    updateEndpoint: 'http://localhost:4030/semem/update',
                    graphName: graph,
                    auth: {
                        user: 'admin',
                        password: 'admin123'
                    }
                });

                // Create a new SearchService with the requested graph
                searchService = new SearchService({
                    embeddingService: this.embeddingService,
                    sparqlService: sparqlService,
                    graphName: graph
                });

                // Initialize the new search service
                await searchService.initialize();
            }

            // Perform search
            const results = await searchService.search(query, limit);

            logger.info(`Found ${results.length} results for query: "${query}" in graph: ${graph}`);

            res.json({ results });
        } catch (error) {
            logger.error('Search error:', error);
            res.status(500).json({
                error: 'Search failed',
                message: error.message
            });
        }
    }

    /**
     * Start the server
     * @returns {Promise<void>}
     */
    async start() {
        try {
            // Configure the app
            this.configureApp();

            // Initialize the search service
            logger.info('Initializing search service...');
            await this.searchService.initialize();

            // Initialize LLM and chat features
            logger.info('Initializing LLM and chat features...');
            await this.initializeChatFeatures();

            // Start the Express server
            this.server = this.app.listen(this.port, () => {
                logger.info(`Search server running at http://localhost:${this.port}`);
            });
        } catch (error) {
            logger.error('Failed to start server:', error);
            throw error;
        }
    }

    /**
     * Create embedding connector using factory
     */
    async createEmbeddingConnector() {
        try {
            // Load system configuration
            const config = new Config();
            await config.init();
            
            // Get embedding provider configuration
            const embeddingProvider = config.get('embeddingProvider') || 'ollama';
            const embeddingModel = config.get('embeddingModel') || 'nomic-embed-text';
            
            logger.info(`Creating embedding connector: ${embeddingProvider} (${embeddingModel})`);
            
            // Create embedding connector using factory
            let providerConfig = {};
            if (embeddingProvider === 'nomic') {
                providerConfig = {
                    provider: 'nomic',
                    apiKey: process.env.NOMIC_API_KEY,
                    model: embeddingModel
                };
            } else if (embeddingProvider === 'ollama') {
                const ollamaBaseUrl = config.get('ollama.baseUrl') || 'http://localhost:11434';
                providerConfig = {
                    provider: 'ollama',
                    baseUrl: ollamaBaseUrl,
                    model: embeddingModel
                };
            }
            
            return EmbeddingConnectorFactory.createConnector(providerConfig);
            
        } catch (error) {
            logger.warn('Failed to create configured embedding connector, falling back to Ollama:', error.message);
            // Fallback to Ollama for embeddings
            return EmbeddingConnectorFactory.createConnector({
                provider: 'ollama',
                baseUrl: 'http://localhost:11434',
                model: 'nomic-embed-text'
            });
        }
    }

    /**
     * Initialize chat features and register API
     */
    async initializeChatFeatures() {
        try {
            // Create embedding connector using configuration
            logger.info('Initializing embedding connector...');
            const embeddingConnector = await this.createEmbeddingConnector();

            // Create memory manager for semantic memory
            logger.info('Initializing memory manager...');
            this.memoryManager = new MemoryManager({
                llmProvider: embeddingConnector, // Use embedding connector as LLM provider
                embeddingProvider: embeddingConnector,
                chatModel: this.chatModel,
                embeddingModel: this.embeddingModel
            });

            // Create LLM handler for direct LLM requests
            logger.info('Initializing LLM handler...');
            this.llmHandler = new LLMHandler(
                embeddingConnector,
                this.chatModel
            );

            // Register core services with API registry
            logger.info('Registering core services with API registry...');
            this.apiRegistry.register('memory', this.memoryManager);
            this.apiRegistry.register('llm', this.llmHandler);

            // Register the chat API
            logger.info('Registering Chat API...');
            await this.apiRegistry.register('chat', ChatAPI, {
                registry: this.apiRegistry,
                similarityThreshold: 0.7,
                contextWindow: 5
            });

            logger.info('Chat features initialized successfully');
        } catch (error) {
            logger.error('Failed to initialize chat features:', error);
            throw error;
        }
    }

    /**
     * Stop the server
     * @returns {Promise<void>}
     */
    async stop() {
        try {
            // Shutdown API services first
            if (this.apiRegistry) {
                logger.info('Shutting down API services...');
                await this.apiRegistry.shutdownAll();
            }

            // Clean up memory manager if needed
            if (this.memoryManager) {
                logger.info('Disposing memory manager...');
                await this.memoryManager.dispose();
            }

            // Stop the HTTP server
            if (this.server) {
                logger.info('Stopping HTTP server...');
                await new Promise((resolve, reject) => {
                    this.server.close((err) => {
                        if (err) {
                            logger.error('Error shutting down server:', err);
                            reject(err);
                        } else {
                            logger.info('Server shut down successfully');
                            resolve();
                        }
                    });
                });
            }
        } catch (error) {
            logger.error('Error during server shutdown:', error);
            throw error;
        }
    }
}

export default SearchServer;