UNPKG

semem

Version:

Semantic Memory for Intelligent Agents

283 lines (248 loc) 10.8 kB
import BaseAPI from '../common/BaseAPI.js'; import { v4 as uuidv4 } from 'uuid'; /** * Search API handler for semantic search capabilities */ export default class SearchAPI extends BaseAPI { constructor(config = {}) { super(config); this.memoryManager = null; this.searchService = null; this.similarityThreshold = config.similarityThreshold || 0.7; this.defaultLimit = config.defaultLimit || 5; } async initialize() { await super.initialize(); // Get dependencies from registry const registry = this.config.registry; if (!registry) { throw new Error('Registry is required for SearchAPI'); } try { this.memoryManager = registry.get('memory'); // SearchService is optional, try to get it if available try { this.searchService = registry.get('search'); } catch (error) { this.logger.warn('Search service not available, using memory manager for search'); } this.logger.info('SearchAPI initialized successfully'); } catch (error) { this.logger.error('Failed to initialize SearchAPI:', error); throw error; } } /** * Execute a search operation */ async executeOperation(operation, params) { this._validateParams(params); const start = Date.now(); try { let result; switch (operation) { case 'search': result = await this.searchContent(params); break; case 'index': result = await this.indexContent(params); break; default: throw new Error(`Unknown operation: ${operation}`); } const duration = Date.now() - start; this._emitMetric(`search.${operation}.duration`, duration); this._emitMetric(`search.${operation}.count`, 1); return result; } catch (error) { this._emitMetric(`search.${operation}.errors`, 1); throw error; } } /** * Search content using semantic similarity */ async searchContent({ query, limit = this.defaultLimit, types, threshold }) { if (!query) { throw new Error('Query is required'); } try { const similarityThreshold = threshold || this.similarityThreshold; // Use dedicated search service if available, otherwise fall back to memory manager let results = []; try { if (this.searchService) { this.logger.info('Using search service for query:', query); results = await this.searchService.search(query, { threshold: similarityThreshold, limit, types: types ? types.split(',') : undefined }); } else { // Fall back to memory manager for search this.logger.info('Using memory manager fallback for search'); try { // Generate embedding safely const embedding = await this.memoryManager.generateEmbedding(query); this.logger.info('Embedding generated successfully'); // Retrieve interactions safely let memories = []; try { memories = await this.memoryManager.retrieveRelevantInteractions( query, similarityThreshold, 0, limit ); this.logger.info(`Retrieved ${memories?.length || 0} memories`); } catch (retrievalError) { this.logger.error('Error retrieving interactions:', retrievalError); memories = []; // Fallback to empty array } // Ensure memories is an array before processing if (!Array.isArray(memories)) { this.logger.warn('Memory retrieval did not return an array'); memories = []; } // Apply safe transformation with defensive coding results = memories .filter(item => item && typeof item === 'object' && item.interaction) .map(item => { try { const interaction = item.interaction || {}; return { id: interaction.id || uuidv4(), title: (interaction.metadata && interaction.metadata.title) || (interaction.prompt && interaction.prompt.slice(0, 50)) || 'Untitled', content: `${interaction.prompt || ''}\n${interaction.output || ''}`, similarity: typeof item.similarity === 'number' ? item.similarity : 0, type: (interaction.metadata && interaction.metadata.type) || 'memory', metadata: interaction.metadata || {} }; } catch (transformError) { this.logger.error('Error transforming memory item:', transformError); // Return a default item on transform error return { id: uuidv4(), title: 'Error Item', content: 'Could not process this item', similarity: 0, type: 'error', metadata: {} }; } }); } catch (embeddingError) { this.logger.error('Error generating embedding:', embeddingError); // Return empty results on embedding error results = []; } } } catch (searchError) { this.logger.error('Error during search process:', searchError); // Return empty results on search error results = []; } this._emitMetric('search.count', 1); return { results, count: results.length }; } catch (error) { this.logger.error('Search operation error:', error); this._emitMetric('search.errors', 1); // Return empty results rather than throwing, for API stability return { results: [], count: 0, error: error.message }; } } /** * Index content for search */ async indexContent({ content, type, title, metadata = {} }) { if (!content) { throw new Error('Content is required'); } if (!type) { throw new Error('Content type is required'); } try { let result; // Use dedicated search service if available if (this.searchService) { const id = uuidv4(); await this.searchService.index({ id, content, type, title: title || content.slice(0, 50), metadata }); result = { id, success: true }; } else { // Fall back to storing in memory const embedding = await this.memoryManager.generateEmbedding(content); const concepts = await this.memoryManager.extractConcepts(content); const id = uuidv4(); await this.memoryManager.addInteraction( title || content.slice(0, 50), content, embedding, concepts, { id, type, timestamp: Date.now(), ...metadata } ); result = { id, success: true }; } this._emitMetric('index.count', 1); return result; } catch (error) { this._emitMetric('index.errors', 1); throw error; } } /** * Get search API metrics */ async getMetrics() { const baseMetrics = await super.getMetrics(); const serviceInfo = this.searchService ? { service: 'dedicated', indexSize: await this._getIndexSize() } : { service: 'memory-fallback' }; return { ...baseMetrics, search: { ...serviceInfo, operations: { search: { count: await this._getMetricValue('search.count'), errors: await this._getMetricValue('search.errors'), duration: await this._getMetricValue('search.duration') }, index: { count: await this._getMetricValue('index.count'), errors: await this._getMetricValue('index.errors') } } } }; } async _getIndexSize() { if (this.searchService && typeof this.searchService.getIndexSize === 'function') { return await this.searchService.getIndexSize(); } return 0; } async _getMetricValue(metricName) { // In a real implementation, this would fetch from a metrics store return 0; } }