UNPKG

@vfarcic/dot-ai

Version:

AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance

325 lines (324 loc) 11.6 kB
"use strict"; /** * Base Vector Service * * Generic vector operations that can be extended for different data types * (patterns, capabilities, dependencies, etc.) * * PRD #359: Now calls plugin directly instead of going through VectorDBService. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseVectorService = void 0; const embedding_service_1 = require("./embedding-service"); const plugin_registry_1 = require("./plugin-registry"); const PLUGIN_NAME = 'agentic-tools'; /** * Abstract base class for vector-based data services */ class BaseVectorService { embeddingService; collectionName; constructor(collectionName, embeddingService) { this.collectionName = collectionName; this.embeddingService = embeddingService || new embedding_service_1.EmbeddingService(); } /** * Invoke a plugin tool and extract the result * @throws Error if plugin returns an error response */ async invokePlugin(tool, args) { const response = await (0, plugin_registry_1.invokePluginTool)(PLUGIN_NAME, tool, args); if (!response.success) { const error = response.error; const message = typeof error === 'object' && error?.message ? error.message : String(error || `Plugin tool ${tool} failed`); throw new Error(message); } // Plugin tools return ToolResult: { success, data, message, error? } // We need to extract the data field, and check for tool-level errors const toolResult = response.result; // Handle missing or malformed result if (!toolResult || typeof toolResult !== 'object') { throw new Error(`Plugin tool ${tool} returned invalid result`); } // Check for tool-level errors (use strict equality to handle undefined gracefully) if (toolResult.success === false) { throw new Error(toolResult.error || toolResult.message || `Plugin tool ${tool} failed`); } return toolResult.data; } /** * Initialize the collection */ async initialize() { // Use embedding dimensions if available, otherwise default to 1536 (OpenAI default) const dimensions = this.embeddingService.isAvailable() ? this.embeddingService.getDimensions() : 1536; await this.invokePlugin('collection_initialize', { collection: this.collectionName, vectorSize: dimensions, createTextIndex: true, }); } /** * Check if collection exists without creating it */ async collectionExists() { try { const stats = await this.invokePlugin('collection_stats', { collection: this.collectionName, }); return stats.exists; } catch { return false; } } /** * Health check for Vector DB connection */ async healthCheck() { if (!(0, plugin_registry_1.isPluginInitialized)()) { return false; } try { await this.invokePlugin('collection_stats', { collection: this.collectionName, }); return true; } catch { return false; } } /** * Store data in Vector DB with semantic embedding */ async storeData(data) { const searchText = this.createSearchText(data); const id = this.extractId(data); // Generate embedding - required for vector storage let embedding; if (this.embeddingService.isAvailable()) { try { embedding = await this.embeddingService.generateEmbedding(searchText); } catch (error) { // Fail immediately with clear error about embedding generation throw new Error(`Embedding generation failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error }); } } else { // Embedding service not available - fail with clear error throw new Error('Embedding service not available - cannot store data in vector collection'); } await this.invokePlugin('vector_store', { collection: this.collectionName, id, embedding, payload: { ...this.createPayload(data), searchText: searchText, hasEmbedding: true, }, }); } /** * Search for data using hybrid semantic + keyword matching */ async searchData(query, options = {}) { // Fail immediately if embedding service not available - no graceful fallback if (!this.embeddingService.isAvailable()) { throw new Error('Embedding service not available - cannot perform semantic search'); } // Extract keywords for keyword search const queryKeywords = this.extractKeywords(query); if (queryKeywords.length === 0) { return []; } const limit = options.limit || 10; const scoreThreshold = options.scoreThreshold || 0.01; // Perform hybrid search (semantic + keyword) try { return await this.hybridSearch(query, queryKeywords, { limit, scoreThreshold, filter: options.filter, }); } catch (error) { // Fail immediately - no fallback to keyword-only search throw new Error(`Semantic search failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error }); } } /** * Get data by ID */ async getData(id) { const document = await this.invokePlugin('vector_get', { collection: this.collectionName, id, }); if (!document) { return null; } const data = this.payloadToData(document.payload); // Set the ID from the document data.id = document.id; return data; } /** * Delete data by ID */ async deleteData(id) { await this.invokePlugin('vector_delete', { collection: this.collectionName, id, }); } /** * Delete all data (preserves collection structure) */ async deleteAllData() { await this.invokePlugin('vector_delete_all', { collection: this.collectionName, }); } /** * Query data with Qdrant filter (no semantic search) * @param filter - Qdrant filter object constructed by AI * @param limit - Maximum results to return */ async queryWithFilter(filter, limit = 100) { const documents = await this.invokePlugin('vector_query', { collection: this.collectionName, filter, limit, }); return documents.map(doc => { const data = this.payloadToData(doc.payload); data.id = doc.id; return data; }); } /** * Get all data (limited) */ async getAllData(limit) { const documents = await this.invokePlugin('vector_list', { collection: this.collectionName, limit: limit ?? 10000, }); return documents.map(doc => { const data = this.payloadToData(doc.payload); data.id = doc.id; return data; }); } /** * Get total count of data items */ async getDataCount() { try { const stats = await this.invokePlugin('collection_stats', { collection: this.collectionName, }); return stats.pointsCount || 0; } catch { // Fallback: get all and count const data = await this.getAllData(); return data.length; } } /** * Get current search mode (semantic vs keyword-only) */ getSearchMode() { const status = this.embeddingService.getStatus(); return { semantic: status.available, provider: status.provider || undefined, reason: status.reason || (status.available ? 'Embedding service available' : undefined), }; } // Virtual methods that can be overridden by subclasses extractKeywords(query) { return query .toLowerCase() .split(/\s+/) .filter(word => word.length > 2); } /** * Hybrid search combining semantic and keyword matching */ async hybridSearch(query, queryKeywords, options) { // Generate query embedding - required for semantic search const queryEmbedding = await this.embeddingService.generateEmbedding(query); if (!queryEmbedding) { throw new Error('Failed to generate query embedding for semantic search'); } // Semantic search using vector similarity const semanticResults = await this.invokePlugin('vector_search', { collection: this.collectionName, embedding: queryEmbedding, limit: options.limit * 2, // Get more candidates for hybrid ranking scoreThreshold: 0.1, // Very permissive threshold for single-word queries filter: options.filter, }); // Keyword search const keywordResults = await this.invokePlugin('vector_search_keywords', { collection: this.collectionName, keywords: queryKeywords, limit: options.limit * 2, filter: options.filter, }); // Combine and rank results return this.combineHybridResults(semanticResults, keywordResults, queryKeywords, options); } /** * Combine semantic and keyword results with hybrid ranking * Keyword matches are prioritized for exact term matches */ combineHybridResults(semanticResults, keywordResults, queryKeywords, options) { const combinedResults = new Map(); // Add semantic results with weighted score for (const result of semanticResults) { const data = this.payloadToData(result.payload); data.id = result.id; combinedResults.set(result.id, { data, score: result.score * 0.5, // Semantic gets 50% weight matchType: 'semantic', }); } // Add or boost keyword results for (const result of keywordResults) { const existing = combinedResults.get(result.id); if (existing) { // Hybrid match: ADD keyword score to semantic score (capped at 1.0) // This ensures exact keyword matches rank higher existing.score = Math.min(1.0, existing.score + result.score * 0.5); existing.matchType = 'hybrid'; } else { const data = this.payloadToData(result.payload); data.id = result.id; combinedResults.set(result.id, { data, score: result.score * 0.5, // Keyword-only gets 50% weight matchType: 'keyword', }); } } // Sort by score and apply limits return Array.from(combinedResults.values()) .filter(result => result.score >= options.scoreThreshold) .sort((a, b) => b.score - a.score) .slice(0, options.limit); } } exports.BaseVectorService = BaseVectorService;