Skip to main content
Glama
unified-database-complete.service.ts31.4 kB
import { Database } from 'sqlite3'; import * as path from 'path'; import * as fs from 'fs'; import OpenAI from 'openai'; import crypto from 'crypto'; // ==================== COMPLETE INTERFACE DEFINITIONS ==================== export interface DecisionEntry { id?: number; summary: string; rationale?: string; implementation_details?: string; tags?: string[]; timestamp?: string; vector_embedding?: Buffer; embedding_model?: string; embedding_created_at?: string; embedding_hash?: string; } export interface CustomDataEntry { id?: number; category: string; key: string; value: any; metadata?: any; timestamp?: string; vector_embedding?: Buffer; embedding_model?: string; embedding_created_at?: string; embedding_hash?: string; } export interface SystemPatternEntry { id?: number; name: string; description?: string; tags?: string[]; timestamp?: string; vector_embedding?: Buffer; embedding_model?: string; embedding_created_at?: string; embedding_hash?: string; } export interface ProgressEntry { id?: number; description: string; status: string; parent_id?: number; linked_item_type?: string; linked_item_id?: string; link_relationship_type?: string; timestamp?: string; vector_embedding?: Buffer; embedding_model?: string; embedding_created_at?: string; embedding_hash?: string; } export interface ContextEntry { id?: number; type: 'product_context' | 'active_context'; content: any; version: number; timestamp: string; } export interface LinkSuggestion { suggestion_id?: number; source_item_type: string; source_item_id: string; target_item_type: string; target_item_id: string; suggested_relationship_type?: string; ai_confidence_score?: number; ai_reasoning?: string; status: string; created_timestamp?: string; updated_timestamp?: string; user_feedback?: string; reviewed_by?: string; model_version?: string; } export interface MetadataSuggestion { suggestion_id?: number; item_type: string; item_id: string; metadata_type: string; suggested_content: string; ai_confidence_score?: number; ai_reasoning?: string; status: string; created_timestamp?: string; updated_timestamp?: string; user_feedback?: string; reviewed_by?: string; model_version?: string; } export interface RagFeedback { feedback_id?: number; rag_query_id?: string; query_text: string; generated_answer: string; feedback_data: any; retrieved_context_summary?: string; evaluator_id?: string; created_timestamp?: string; } export interface ItemLink { link_id?: number; source_item_type: string; source_item_id: string; target_item_type: string; target_item_id: string; relationship_type: string; description?: string; created_timestamp?: string; } export interface SemanticSearchResult { item_id: number; item_type: string; content_text: string; similarity_score: number; metadata?: any; tags?: string[]; timestamp?: string; } export interface EmbeddingStats { item_type: string; total_items: number; items_with_embeddings: number; items_without_embeddings: number; embedding_coverage_percentage: number; latest_embedding_created: string | null; } export interface RecentActivitySummary { decisions: DecisionEntry[]; progress_entries: ProgressEntry[]; system_patterns: SystemPatternEntry[]; custom_data: CustomDataEntry[]; context_updates: ContextEntry[]; } // ==================== COMPLETE DATABASE SERVICE ==================== export class UnifiedDatabaseService { private db: Database; private openai?: OpenAI; private dbPath: string; private embeddingModel = 'text-embedding-3-small'; constructor(workspaceId: string) { this.dbPath = path.join(workspaceId, 'sdof_unified_knowledge.db'); const dbDir = path.dirname(this.dbPath); if (!fs.existsSync(dbDir)) { fs.mkdirSync(dbDir, { recursive: true }); } this.db = new Database(this.dbPath); const apiKey = process.env.OPENAI_API_KEY; if (!apiKey) { console.warn('OPENAI_API_KEY not set - vector embeddings will be disabled'); } else { this.openai = new OpenAI({ apiKey }); } this.initializeTables(); } private initializeTables(): void { const tables = [ // Core tables with vector embedding support `CREATE TABLE IF NOT EXISTS decisions ( id INTEGER PRIMARY KEY AUTOINCREMENT, summary TEXT NOT NULL, rationale TEXT, implementation_details TEXT, tags TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, vector_embedding BLOB, embedding_model TEXT, embedding_created_at DATETIME, embedding_hash TEXT )`, `CREATE TABLE IF NOT EXISTS custom_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, category TEXT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, metadata TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, vector_embedding BLOB, embedding_model TEXT, embedding_created_at DATETIME, embedding_hash TEXT, UNIQUE(category, key) )`, `CREATE TABLE IF NOT EXISTS system_patterns ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, description TEXT, tags TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, vector_embedding BLOB, embedding_model TEXT, embedding_created_at DATETIME, embedding_hash TEXT )`, `CREATE TABLE IF NOT EXISTS progress_entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, description TEXT NOT NULL, status TEXT NOT NULL, parent_id INTEGER, linked_item_type TEXT, linked_item_id TEXT, link_relationship_type TEXT DEFAULT 'relates_to_progress', timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, vector_embedding BLOB, embedding_model TEXT, embedding_created_at DATETIME, embedding_hash TEXT, FOREIGN KEY (parent_id) REFERENCES progress_entries (id) )`, `CREATE TABLE IF NOT EXISTS context_entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL CHECK (type IN ('product_context', 'active_context')), content TEXT NOT NULL, version INTEGER NOT NULL DEFAULT 1, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP )`, // AI Enrichment tables `CREATE TABLE IF NOT EXISTS conport_link_suggestions ( suggestion_id INTEGER PRIMARY KEY AUTOINCREMENT, source_item_type TEXT NOT NULL, source_item_id TEXT NOT NULL, target_item_type TEXT NOT NULL, target_item_id TEXT NOT NULL, suggested_relationship_type TEXT, ai_confidence_score REAL, ai_reasoning TEXT, status TEXT DEFAULT 'pending', created_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, updated_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, user_feedback TEXT, reviewed_by TEXT, model_version TEXT )`, `CREATE TABLE IF NOT EXISTS conport_metadata_suggestions ( suggestion_id INTEGER PRIMARY KEY AUTOINCREMENT, item_type TEXT NOT NULL, item_id TEXT NOT NULL, metadata_type TEXT NOT NULL, suggested_content TEXT NOT NULL, ai_confidence_score REAL, ai_reasoning TEXT, status TEXT DEFAULT 'pending', created_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, updated_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, user_feedback TEXT, reviewed_by TEXT, model_version TEXT )`, `CREATE TABLE IF NOT EXISTS conport_rag_feedback ( feedback_id INTEGER PRIMARY KEY AUTOINCREMENT, rag_query_id TEXT, query_text TEXT NOT NULL, generated_answer TEXT NOT NULL, feedback_data TEXT NOT NULL, retrieved_context_summary TEXT, evaluator_id TEXT, created_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP )`, `CREATE TABLE IF NOT EXISTS item_links ( link_id INTEGER PRIMARY KEY AUTOINCREMENT, source_item_type TEXT NOT NULL, source_item_id TEXT NOT NULL, target_item_type TEXT NOT NULL, target_item_id TEXT NOT NULL, relationship_type TEXT NOT NULL, description TEXT, created_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP )`, // SQLite-vec vector search table `CREATE VIRTUAL TABLE IF NOT EXISTS vec_embeddings USING vec0( item_type TEXT, item_id TEXT, embedding FLOAT[1536], content_text TEXT, metadata TEXT )` ]; tables.forEach((sql, index) => { this.db.run(sql, (err) => { if (err) { console.error(`Error creating table ${index + 1}:`, err); } }); }); // Initialize context entries with empty records this.db.run(`INSERT OR IGNORE INTO context_entries (id, type, content, version) VALUES (1, 'product_context', '{}', 1), (2, 'active_context', '{}', 1)`); } // ==================== CORE CONTEXT METHODS ==================== getProductContext(): Promise<any> { return new Promise((resolve, reject) => { this.db.get(`SELECT content FROM context_entries WHERE type = 'product_context' ORDER BY version DESC LIMIT 1`, (err, row: any) => { if (err) reject(err); else resolve(row ? JSON.parse(row.content) : {}); } ); }); } updateProductContext(content?: any, patchContent?: any): Promise<{ status: string; message: string }> { return new Promise(async (resolve, reject) => { try { const currentContent = await this.getProductContext(); let newContent: any; if (content !== undefined) { newContent = content; } else if (patchContent !== undefined) { newContent = { ...currentContent }; for (const [key, value] of Object.entries(patchContent)) { if (value === '__DELETE__') { delete newContent[key]; } else { newContent[key] = value; } } } else { return reject(new Error('Either content or patch_content must be provided')); } const version = await this.getNextVersion('product_context'); this.db.run(`INSERT INTO context_entries (type, content, version) VALUES (?, ?, ?)`, ['product_context', JSON.stringify(newContent), version], function(err) { if (err) reject(err); else resolve({ status: 'success', message: 'Product context updated successfully' }); } ); } catch (error) { reject(error); } }); } getActiveContext(): Promise<any> { return new Promise((resolve, reject) => { this.db.get(`SELECT content FROM context_entries WHERE type = 'active_context' ORDER BY version DESC LIMIT 1`, (err, row: any) => { if (err) reject(err); else resolve(row ? JSON.parse(row.content) : {}); } ); }); } updateActiveContext(content?: any, patchContent?: any): Promise<{ status: string; message: string }> { return new Promise(async (resolve, reject) => { try { const currentContent = await this.getActiveContext(); let newContent: any; if (content !== undefined) { newContent = content; } else if (patchContent !== undefined) { newContent = { ...currentContent }; for (const [key, value] of Object.entries(patchContent)) { if (value === '__DELETE__') { delete newContent[key]; } else { newContent[key] = value; } } } else { return reject(new Error('Either content or patch_content must be provided')); } const version = await this.getNextVersion('active_context'); this.db.run(`INSERT INTO context_entries (type, content, version) VALUES (?, ?, ?)`, ['active_context', JSON.stringify(newContent), version], function(err) { if (err) reject(err); else resolve({ status: 'success', message: 'Active context updated successfully' }); } ); } catch (error) { reject(error); } }); } // ==================== DECISION METHODS ==================== async logDecision(entry: DecisionEntry): Promise<DecisionEntry> { return new Promise(async (resolve, reject) => { const timestamp = new Date().toISOString(); const tagsJson = entry.tags ? JSON.stringify(entry.tags) : null; this.db.run(`INSERT INTO decisions (summary, rationale, implementation_details, tags, timestamp) VALUES (?, ?, ?, ?, ?)`, [entry.summary, entry.rationale, entry.implementation_details, tagsJson, timestamp], async (err: any) => { if (err) { reject(err); } else { const insertResult = this as any; const newEntry: DecisionEntry = { id: insertResult.lastID, ...entry, timestamp }; // Generate embedding for the decision try { await this.generateAndStoreEmbedding('decision', insertResult.lastID.toString(), { summary: entry.summary, rationale: entry.rationale, implementation_details: entry.implementation_details }); } catch (embErr) { console.warn('Failed to generate embedding for decision:', embErr); } resolve(newEntry); } } ); }); } getDecisions(limit?: number, tagsFilterIncludeAll?: string[], tagsFilterIncludeAny?: string[]): Promise<DecisionEntry[]> { return new Promise((resolve, reject) => { let sql = 'SELECT * FROM decisions'; const params: any[] = []; const conditions: string[] = []; if (tagsFilterIncludeAll && tagsFilterIncludeAll.length > 0) { const tagConditions = tagsFilterIncludeAll.map(() => "json_extract(tags, '$') LIKE ?"); conditions.push(`(${tagConditions.join(' AND ')})`); tagsFilterIncludeAll.forEach(tag => params.push(`%"${tag}"%`)); } if (tagsFilterIncludeAny && tagsFilterIncludeAny.length > 0) { const tagConditions = tagsFilterIncludeAny.map(() => "json_extract(tags, '$') LIKE ?"); conditions.push(`(${tagConditions.join(' OR ')})`); tagsFilterIncludeAny.forEach(tag => params.push(`%"${tag}"%`)); } if (conditions.length > 0) { sql += ' WHERE ' + conditions.join(' AND '); } sql += ' ORDER BY timestamp DESC'; if (limit) { sql += ' LIMIT ?'; params.push(limit); } this.db.all(sql, params, (err, rows: any[]) => { if (err) { reject(err); } else { const decisions = rows.map(row => ({ ...row, tags: row.tags ? JSON.parse(row.tags) : [] })); resolve(decisions); } }); }); } searchDecisionsFts(queryTerm: string, limit: number = 10): Promise<DecisionEntry[]> { return new Promise((resolve, reject) => { const sql = `SELECT * FROM decisions WHERE summary LIKE ? OR rationale LIKE ? OR implementation_details LIKE ? ORDER BY timestamp DESC LIMIT ?`; const searchTerm = `%${queryTerm}%`; this.db.all(sql, [searchTerm, searchTerm, searchTerm, limit], (err, rows: any[]) => { if (err) { reject(err); } else { const decisions = rows.map(row => ({ ...row, tags: row.tags ? JSON.parse(row.tags) : [] })); resolve(decisions); } }); }); } deleteDecisionById(id: number): Promise<{ status: string; message: string }> { return new Promise((resolve, reject) => { this.db.run('DELETE FROM decisions WHERE id = ?', [id], function(err) { if (err) { reject(err); } else if (this.changes === 0) { reject(new Error(`Decision with ID ${id} not found`)); } else { resolve({ status: 'success', message: `Decision ${id} deleted successfully` }); } }); }); } // ==================== CUSTOM DATA METHODS ==================== async logCustomData(entry: CustomDataEntry): Promise<CustomDataEntry> { return new Promise(async (resolve, reject) => { const timestamp = new Date().toISOString(); const valueJson = JSON.stringify(entry.value); const metadataJson = entry.metadata ? JSON.stringify(entry.metadata) : null; this.db.run(`INSERT OR REPLACE INTO custom_data (category, key, value, metadata, timestamp) VALUES (?, ?, ?, ?, ?)`, [entry.category, entry.key, valueJson, metadataJson, timestamp], async (err: any) => { if (err) { reject(err); } else { const insertResult = this as any; const newEntry: CustomDataEntry = { id: insertResult.lastID, ...entry, timestamp }; // Generate embedding for custom data try { await this.generateAndStoreEmbedding('custom_data', insertResult.lastID.toString(), { category: entry.category, key: entry.key, value: entry.value }); } catch (embErr) { console.warn('Failed to generate embedding for custom data:', embErr); } resolve(newEntry); } } ); }); } getCustomData(category?: string, key?: string): Promise<CustomDataEntry[]> { return new Promise((resolve, reject) => { let sql = 'SELECT * FROM custom_data'; const params: any[] = []; if (category && key) { sql += ' WHERE category = ? AND key = ?'; params.push(category, key); } else if (category) { sql += ' WHERE category = ?'; params.push(category); } sql += ' ORDER BY timestamp DESC'; this.db.all(sql, params, (err, rows: any[]) => { if (err) { reject(err); } else { const customData = rows.map(row => ({ ...row, value: JSON.parse(row.value), metadata: row.metadata ? JSON.parse(row.metadata) : null })); resolve(customData); } }); }); } deleteCustomData(category: string, key: string): Promise<{ status: string; message: string }> { return new Promise((resolve, reject) => { this.db.run('DELETE FROM custom_data WHERE category = ? AND key = ?', [category, key], function(err) { if (err) { reject(err); } else if (this.changes === 0) { reject(new Error(`Custom data with category "${category}" and key "${key}" not found`)); } else { resolve({ status: 'success', message: `Custom data deleted successfully` }); } }); }); } searchCustomDataValuesFts(queryTerm: string, categoryFilter?: string, limit: number = 10): Promise<CustomDataEntry[]> { return new Promise((resolve, reject) => { let sql = `SELECT * FROM custom_data WHERE category LIKE ? OR key LIKE ? OR value LIKE ?`; const params = [`%${queryTerm}%`, `%${queryTerm}%`, `%${queryTerm}%`]; if (categoryFilter) { sql += ' AND category = ?'; params.push(categoryFilter); } sql += ' ORDER BY timestamp DESC LIMIT ?'; params.push(limit.toString()); this.db.all(sql, params, (err, rows: any[]) => { if (err) { reject(err); } else { const customData = rows.map(row => ({ ...row, value: JSON.parse(row.value), metadata: row.metadata ? JSON.parse(row.metadata) : null })); resolve(customData); } }); }); } searchProjectGlossaryFts(queryTerm: string, limit: number = 10): Promise<CustomDataEntry[]> { return this.searchCustomDataValuesFts(queryTerm, 'ProjectGlossary', limit); } // ==================== EMBEDDING AND VECTOR METHODS ==================== private async generateEmbedding(text: string): Promise<number[]> { if (!this.openai) { throw new Error('OpenAI not configured - cannot generate embeddings'); } const response = await this.openai.embeddings.create({ model: this.embeddingModel, input: text.substring(0, 8000) // Truncate to avoid token limits }); return response.data[0].embedding; } private generateContentHash(content: any): string { return crypto.createHash('md5').update(JSON.stringify(content)).digest('hex'); } private async generateAndStoreEmbedding(itemType: string, itemId: string, content: any): Promise<void> { if (!this.openai) return; try { const contentText = this.extractTextForEmbedding(content); const contentHash = this.generateContentHash(content); // Check if embedding already exists with same hash const existingHash = await this.getEmbeddingHash(itemType, itemId); if (existingHash === contentHash) { return; // No need to regenerate } const embedding = await this.generateEmbedding(contentText); const now = new Date().toISOString(); // Store in vector table await this.storeVectorEmbedding(itemType, itemId, embedding, contentText, content); // Update main table with embedding metadata const updateSql = `UPDATE ${this.getTableName(itemType)} SET embedding_hash = ?, embedding_model = ?, embedding_created_at = ? WHERE id = ?`; this.db.run(updateSql, [contentHash, this.embeddingModel, now, itemId]); } catch (error) { console.error(`Failed to generate embedding for ${itemType} ${itemId}:`, error); throw error; } } private extractTextForEmbedding(content: any): string { if (typeof content === 'string') return content; if (typeof content === 'object') { return Object.values(content).filter(v => typeof v === 'string').join(' '); } return String(content); } private getTableName(itemType: string): string { const mapping: Record<string, string> = { 'decision': 'decisions', 'custom_data': 'custom_data', 'system_pattern': 'system_patterns', 'progress_entry': 'progress_entries' }; return mapping[itemType] || itemType; } private getEmbeddingHash(itemType: string, itemId: string): Promise<string | null> { return new Promise((resolve, reject) => { const tableName = this.getTableName(itemType); this.db.get(`SELECT embedding_hash FROM ${tableName} WHERE id = ?`, [itemId], (err, row: any) => { if (err) reject(err); else resolve(row?.embedding_hash || null); }); }); } async storeVectorEmbedding(itemType: string, itemId: string, vector: number[], contentText: string, metadata: any): Promise<void> { return new Promise((resolve, reject) => { const metadataJson = JSON.stringify(metadata); this.db.run(`INSERT OR REPLACE INTO vec_embeddings (item_type, item_id, embedding, content_text, metadata) VALUES (?, ?, ?, ?, ?)`, [itemType, itemId, JSON.stringify(vector), contentText, metadataJson], function(err) { if (err) reject(err); else resolve(); } ); }); } async searchVectors(query: string, limit: number = 10): Promise<SemanticSearchResult[]> { if (!this.openai) { throw new Error('OpenAI not configured - cannot perform vector search'); } try { const queryEmbedding = await this.generateEmbedding(query); return new Promise((resolve, reject) => { // Use SQLite-vec for similarity search const sql = `SELECT item_type, item_id, content_text, metadata, vec_distance_cosine(embedding, ?) as distance FROM vec_embeddings ORDER BY distance ASC LIMIT ?`; this.db.all(sql, [JSON.stringify(queryEmbedding), limit], (err, rows: any[]) => { if (err) { reject(err); } else { const results: SemanticSearchResult[] = rows.map(row => ({ item_id: parseInt(row.item_id), item_type: row.item_type, content_text: row.content_text, similarity_score: 1 - row.distance, // Convert distance to similarity metadata: JSON.parse(row.metadata || '{}') })); resolve(results); } }); }); } catch (error) { console.error('Vector search failed:', error); throw error; } } async batchProcessEmbeddings(): Promise<{ processed: number; errors: number }> { let processed = 0; let errors = 0; try { // Process each item type const itemTypes = ['decision', 'custom_data', 'system_pattern', 'progress_entry']; for (const itemType of itemTypes) { const items = await this.getItemsWithoutEmbeddings(itemType); for (const item of items) { try { await this.generateAndStoreEmbedding(itemType, item.id.toString(), item); processed++; } catch (error) { console.error(`Failed to process embedding for ${itemType} ${item.id}:`, error); errors++; } } } return { processed, errors }; } catch (error) { console.error('Batch embedding processing failed:', error); throw error; } } private getItemsWithoutEmbeddings(itemType: string): Promise<any[]> { return new Promise((resolve, reject) => { const tableName = this.getTableName(itemType); const sql = `SELECT * FROM ${tableName} WHERE embedding_hash IS NULL`; this.db.all(sql, (err, rows: any[]) => { if (err) reject(err); else resolve(rows); }); }); } async getEmbeddingStats(): Promise<EmbeddingStats[]> { const itemTypes = ['decision', 'custom_data', 'system_pattern', 'progress_entry']; const stats: EmbeddingStats[] = []; for (const itemType of itemTypes) { const tableName = this.getTableName(itemType); const totalItems = await this.getCount(tableName); const itemsWithEmbeddings = await this.getCount(tableName, 'embedding_hash IS NOT NULL'); const itemsWithoutEmbeddings = totalItems - itemsWithEmbeddings; const coverage = totalItems > 0 ? (itemsWithEmbeddings / totalItems) * 100 : 0; const latestEmbedding = await this.getLatestEmbeddingDate(tableName); stats.push({ item_type: itemType, total_items: totalItems, items_with_embeddings: itemsWithEmbeddings, items_without_embeddings: itemsWithoutEmbeddings, embedding_coverage_percentage: Math.round(coverage * 100) / 100, latest_embedding_created: latestEmbedding }); } return stats; } private getCount(tableName: string, condition?: string): Promise<number> { return new Promise((resolve, reject) => { const sql = `SELECT COUNT(*) as count FROM ${tableName}${condition ? ' WHERE ' + condition : ''}`; this.db.get(sql, (err, row: any) => { if (err) reject(err); else resolve(row.count); }); }); } private getLatestEmbeddingDate(tableName: string): Promise<string | null> { return new Promise((resolve, reject) => { const sql = `SELECT MAX(embedding_created_at) as latest FROM ${tableName} WHERE embedding_hash IS NOT NULL`; this.db.get(sql, (err, row: any) => { if (err) reject(err); else resolve(row.latest); }); }); } // ==================== HELPER METHODS ==================== private getNextVersion(contextType: string): Promise<number> { return new Promise((resolve, reject) => { this.db.get(`SELECT MAX(version) as max_version FROM context_entries WHERE type = ?`, [contextType], (err, row: any) => { if (err) reject(err); else resolve((row?.max_version || 0) + 1); } ); }); } // ==================== SEMANTIC SEARCH ==================== async semanticSearchConport(queryText: string, topK: number = 5, filterItemTypes?: string[], filterTagsIncludeAny?: string[], filterTagsIncludeAll?: string[], filterCustomDataCategories?: string[]): Promise<SemanticSearchResult[]> { if (!this.openai) { console.warn('OpenAI not configured - falling back to keyword search'); return this.fallbackKeywordSearch(queryText, topK); } try { const queryEmbedding = await this.generateEmbedding(queryText); return new Promise((resolve, reject) => { let sql = `SELECT item_type, item_id, content_text, metadata, vec_distance_cosine(embedding, ?) as distance FROM vec_embeddings`; const params = [JSON.stringify(queryEmbedding)]; const conditions: string[] = []; if (filterItemTypes && filterItemTypes.length > 0) { conditions.push(`item_type IN (${filterItemTypes.map(() => '?').join(',')})`); params.push(...filterItemTypes); } if (conditions.length > 0) { sql += ' WHERE ' + conditions.join(' AND '); } sql += ' ORDER BY distance ASC LIMIT ?'; params.push(topK.toString()); this.db.all(sql, params, (err, rows: any[]) => { if (err) { reject(err); } else { const results: SemanticSearchResult[] = rows.map(row => ({ item_id: parseInt(row.item_id), item_type: row.item_type, content_text: row.content_text, similarity_score: 1 - row.distance, metadata: JSON.parse(row.metadata || '{}') })); resolve(results); } }); }); } catch (error) { console.error('Semantic search failed, falling back to keyword search:', error); return this.fallbackKeywordSearch(queryText, topK); } } private async fallbackKeywordSearch(queryText: string, limit: number): Promise<SemanticSearchResult[]> { // Implement basic keyword search across all tables const results: SemanticSearchResult[] = []; // Search decisions const decisions = await this.searchDecisionsFts(queryText, limit); decisions.forEach(d => { results.push({ item_id: d.id!, item_type: 'decision', content_text: `${d.summary} ${d.rationale || ''} ${d.implementation_details || ''}`, similarity_score: 0.8, // Fixed score for keyword match metadata: { tags: d.tags }, timestamp: d.timestamp || new Date().toISOString() }); }); return results.slice(0, limit); } // Close database connection close(): void { this.db.close(); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/tgf-between-your-legs/sdof-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server