UNPKG

hikma-engine

Version:

Code Knowledge Graph Indexer - A sophisticated TypeScript-based indexer that transforms Git repositories into multi-dimensional knowledge stores for AI agents

472 lines (471 loc) 19.4 kB
"use strict"; /** * LLM-based RAG service for generating code explanations * Uses Python backend with code-specialized LLMs */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.adaptSearchResults = adaptSearchResults; exports.generateRAGExplanation = generateRAGExplanation; exports.cleanupRAGService = cleanupRAGService; const child_process_1 = require("child_process"); const path = __importStar(require("path")); const logger_1 = require("../utils/logger"); const python_dependency_checker_1 = require("../utils/python-dependency-checker"); const config_1 = require("../config"); const llm_providers_1 = require("./llm-providers"); // Adapter function to convert EmbeddingSearchResult to SearchResult function adaptSearchResults(results) { return results.map(result => ({ file_path: result.node?.filePath || result.node?.file_path || 'Unknown', node_type: result.node?.nodeType || result.node?.type || 'Unknown', similarity: result.similarity || 0, source_text: result.node?.sourceText || result.node?.source_text || '', // Preserve original structure for additional data ...result })); } const DEFAULT_TIMEOUT = 300000; // 5 minutes // Get the default RAG model from configuration function getDefaultModel() { try { return (0, config_1.getConfig)().getAIConfig().rag.model; } catch { // Fallback to hardcoded value if config is not available return 'Qwen/Qwen2.5-Coder-1.5B-Instruct'; } } class LLMRAGService { constructor() { this.logger = (0, logger_1.getLogger)('LLMRAGService'); this.activeProcess = null; } /** * Generate an explanation using LLM-based RAG * Now delegates to the provider manager for consistency */ async generateExplanation(query, searchResults, options = {}) { const operation = this.logger.operation(`RAG explanation for: "${query.substring(0, 50)}..."`); try { this.logger.info('LLMRAGService delegating to provider manager', { query: query.substring(0, 100), resultCount: searchResults.length, options }); // Delegate to provider manager for consistent behavior const result = await providerManager.generateExplanation(query, searchResults, options); operation(); return result; } catch (error) { operation(); const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error('LLMRAGService explanation error', { error: errorMessage }); // If provider manager fails completely, try direct Python execution as last resort if (errorMessage.includes('No healthy providers available') || errorMessage.includes('All providers failed')) { this.logger.warn('Provider manager has no healthy providers, attempting direct Python execution'); try { return await this.executePythonRAGDirect(query, searchResults, options); } catch (pythonError) { const pythonErrorMessage = pythonError instanceof Error ? pythonError.message : String(pythonError); this.logger.error('Direct Python execution also failed', { error: pythonErrorMessage }); return { success: false, error: `All RAG methods failed. Provider manager: ${errorMessage}. Direct Python: ${pythonErrorMessage}`, model: options.model || getDefaultModel() }; } } return { success: false, error: errorMessage, model: options.model || getDefaultModel() }; } } /** * Direct Python execution as absolute last resort * This maintains the original Python execution logic for emergency fallback */ async executePythonRAGDirect(query, searchResults, options = {}) { const { model = getDefaultModel(), timeout = DEFAULT_TIMEOUT, maxResults = 8 } = options; try { // Only check Python dependencies if the configured provider is python try { const provider = (0, config_1.getConfig)().getAIConfig().llmProvider.provider; if (provider === 'python') { await (0, python_dependency_checker_1.ensurePythonDependencies)(false, false); } } catch { // If config isn't available, skip implicit python deps check } this.logger.info('Starting direct Python RAG execution', { query: query.substring(0, 100), resultCount: searchResults.length, model, timeout }); // Prepare search results (limit to avoid token overflow) const limitedResults = searchResults.slice(0, maxResults); // Execute Python RAG service const result = await this.executePythonRAG(query, limitedResults, model, timeout); if (result.success) { this.logger.info('Direct Python RAG explanation generated successfully', { model: result.model, device: result.device, explanationLength: result.explanation?.length || 0 }); } else { this.logger.warn('Direct Python RAG explanation failed', { error: result.error, model: result.model }); } return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); // Check if this is a Python dependency error if (errorMessage.includes('Python dependencies') || errorMessage.includes('Python 3 is required')) { this.logger.error('Python dependencies not available for RAG', { error: errorMessage }); return { success: false, error: `RAG feature requires Python dependencies: ${errorMessage}`, model }; } this.logger.error('Direct Python RAG explanation error', { error: errorMessage }); return { success: false, error: errorMessage, model: model }; } } /** * Execute Python RAG script and handle communication */ async executePythonRAG(query, searchResults, model, timeout) { return new Promise((resolve, reject) => { // Resolve Python script path - works for both dev (src/) and built (dist/) versions const isBuilt = __dirname.includes('dist'); const pythonScript = isBuilt ? path.join(__dirname, '..', '..', 'src', 'python', 'llm_rag.py') : path.join(__dirname, '..', 'python', 'llm_rag.py'); this.logger.debug('Spawning Python RAG process', { script: pythonScript, model }); const pythonProcess = (0, child_process_1.spawn)('python3', [pythonScript], { stdio: ['pipe', 'pipe', 'pipe'], cwd: path.join(__dirname, '..') }); this.activeProcess = pythonProcess; // Handle stdin errors (like EPIPE) pythonProcess.stdin.on('error', (error) => { this.logger.warn('Python RAG process stdin error', { error: error.message }); // Don't throw here, just log the error }); let stdout = ''; let stderr = ''; let hasResolved = false; // Handle stdout (main response) pythonProcess.stdout.on('data', (data) => { stdout += data.toString(); }); // Handle stderr (status messages and errors) pythonProcess.stderr.on('data', (data) => { const message = data.toString().trim(); stderr += message + '\n'; // Try to parse status messages try { const statusMessage = JSON.parse(message); if (statusMessage.type === 'status') { this.logger.info('Python RAG status', { message: statusMessage.message }); } } catch { // Not JSON, treat as regular stderr if (message.includes('ERROR') || message.includes('Error')) { this.logger.warn('Python RAG stderr', { message }); } else { this.logger.debug('Python RAG stderr', { message }); } } }); // Handle process completion pythonProcess.on('close', (code) => { this.activeProcess = null; if (hasResolved) return; hasResolved = true; if (code === 0) { try { const result = JSON.parse(stdout.trim()); resolve(result); } catch (error) { this.logger.error('Failed to parse Python RAG response', { stdout: stdout.substring(0, 500), stderr: stderr.substring(0, 500), parseError: error instanceof Error ? error.message : String(error) }); reject(new Error(`Failed to parse Python response: ${error}`)); } } else { this.logger.error('Python RAG process failed', { code, stderr: stderr.substring(0, 1000) }); reject(new Error(`Python process failed with code ${code}: ${stderr}`)); } }); // Handle process errors pythonProcess.on('error', (error) => { this.activeProcess = null; if (hasResolved) return; hasResolved = true; this.logger.error('Python RAG process error', { error: error.message }); reject(new Error(`Failed to start Python process: ${error.message}`)); }); // Set timeout const timeoutHandle = setTimeout(() => { if (hasResolved) return; hasResolved = true; this.logger.warn('Python RAG process timeout', { timeout }); if (pythonProcess && !pythonProcess.killed) { pythonProcess.kill('SIGTERM'); // Force kill after 5 seconds setTimeout(() => { if (pythonProcess && !pythonProcess.killed) { pythonProcess.kill('SIGKILL'); } }, 5000); } reject(new Error(`RAG generation timed out after ${timeout}ms`)); }, timeout); // Send input data to Python process const inputData = { query, search_results: searchResults, model }; try { const success = pythonProcess.stdin.write(JSON.stringify(inputData)); if (!success) { this.logger.warn('Python RAG process stdin buffer full, waiting for drain'); } pythonProcess.stdin.end(); } catch (error) { clearTimeout(timeoutHandle); if (hasResolved) return; hasResolved = true; const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error('Failed to send data to Python RAG process', { error: errorMessage }); reject(new Error(`Failed to send data to Python: ${errorMessage}`)); } }); } /** * Cleanup any running processes * Now also handles provider manager cleanup */ async cleanup() { const cleanupPromises = []; // Cleanup active Python process if any if (this.activeProcess && !this.activeProcess.killed) { this.logger.info('Cleaning up active Python RAG process'); this.activeProcess.kill('SIGTERM'); // Wait a bit, then force kill if necessary const processCleanup = new Promise((resolve) => { const timeout = setTimeout(() => { if (this.activeProcess && !this.activeProcess.killed) { this.activeProcess.kill('SIGKILL'); } resolve(); }, 5000); if (this.activeProcess) { this.activeProcess.on('exit', () => { clearTimeout(timeout); resolve(); }); } else { clearTimeout(timeout); resolve(); } }); cleanupPromises.push(processCleanup); } // Wait for all cleanup operations await Promise.allSettled(cleanupPromises); this.logger.info('LLMRAGService cleanup completed'); } } // Global service instances const ragService = new LLMRAGService(); const providerManager = new llm_providers_1.LLMProviderManager(); // Cleanup on process exit process.on('exit', () => { // Synchronous cleanup only - async operations are not allowed in 'exit' handler try { if (ragService['activeProcess'] && !ragService['activeProcess'].killed) { ragService['activeProcess'].kill('SIGKILL'); } } catch { // Ignore cleanup errors on exit } }); // Only register signal handlers if we're not in a CLI context // CLI commands should handle their own exit logic const isCLIContext = process.argv.some(arg => arg.includes('hikma-engine') || arg.includes('cli') || arg.includes('main.js') || arg.includes('embed') || arg.includes('search') || arg.includes('rag')); // Debug: Log detection result const logger = (0, logger_1.getLogger)('ProcessSignalSetup'); logger.debug('CLI context detection', { isCLIContext, argv: process.argv, willRegisterSignalHandlers: !isCLIContext }); if (!isCLIContext) { process.on('SIGINT', async () => { await Promise.allSettled([ ragService.cleanup(), providerManager.cleanup() ]); process.exit(0); }); process.on('SIGTERM', async () => { await Promise.allSettled([ ragService.cleanup(), providerManager.cleanup() ]); process.exit(0); }); } /** * Generate an explanation for search results using LLM-based RAG * Uses the configurable provider system with intelligent fallback * * @param query - The original search query * @param searchResults - Array of search results to explain * @param options - Optional configuration * @returns Promise resolving to RAG response */ async function generateRAGExplanation(query, searchResults, options = {}) { const logger = (0, logger_1.getLogger)('generateRAGExplanation'); const operation = logger.operation(`RAG explanation for: "${query.substring(0, 50)}..."`); try { // Use the provider manager for all requests logger.debug('Generating explanation with provider manager', { queryLength: query.length, resultCount: searchResults.length, options }); const result = await providerManager.generateExplanation(query, searchResults, options); operation(); logger.info('RAG explanation completed successfully', { provider: result.provider || 'unknown', explanationLength: result.explanation?.length || 0, success: result.success }); return result; } catch (error) { operation(); const errorMessage = error instanceof Error ? error.message : String(error); logger.error('RAG explanation failed', { error: errorMessage, queryLength: query.length, resultCount: searchResults.length }); // Return a properly formatted error response return { success: false, error: `RAG explanation failed: ${errorMessage}`, model: options.model || 'unknown', provider: 'error' }; } } /** * Cleanup RAG service resources * Cleans up both the provider manager and legacy Python service * Ensures proper shutdown coordination */ async function cleanupRAGService() { const logger = (0, logger_1.getLogger)('cleanupRAGService'); logger.info('Starting RAG service cleanup'); try { // Cleanup both provider manager and legacy service in parallel await Promise.allSettled([ providerManager.cleanup(), ragService.cleanup() ]); logger.info('RAG service cleanup completed successfully'); } catch (error) { logger.error('Error during RAG service cleanup', { error: error instanceof Error ? error.message : String(error) }); // Don't throw - cleanup should be best effort } }