UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

480 lines (479 loc) 13.9 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { logger } from "../monitoring/logger.js"; class ContextRetriever { adapter; strategies = /* @__PURE__ */ new Map(); queryCache = /* @__PURE__ */ new Map(); cacheMaxSize = 100; cacheExpiryMs = 3e5; // 5 minutes constructor(adapter) { this.adapter = adapter; this.initializeStrategies(); } initializeStrategies() { this.strategies.set("keyword", { name: "Keyword Search", searchType: "text", boost: { name: 2, digest_text: 1.5, inputs: 1.2, outputs: 1.2 }, fallbackStrategy: "semantic" }); this.strategies.set("semantic", { name: "Semantic Search", searchType: "vector", fallbackStrategy: "hybrid" }); this.strategies.set("hybrid", { name: "Hybrid Search", searchType: "hybrid", weights: { text: 0.6, vector: 0.4 }, boost: { name: 2, digest_text: 1.5 }, fallbackStrategy: "keyword" }); this.strategies.set("recent", { name: "Recent Activity", searchType: "text", boost: { created_at: 3, closed_at: 2 }, fallbackStrategy: "hybrid" }); this.strategies.set("debug", { name: "Debug Context", searchType: "hybrid", weights: { text: 0.8, vector: 0.2 }, boost: { type: 2.5, // Boost error frames digest_text: 2, outputs: 1.8 }, fallbackStrategy: "keyword" }); } async retrieveContext(query) { const startTime = Date.now(); if (!query.text || query.text.trim().length === 0) { logger.debug("Empty query provided, returning empty result"); return { contexts: [], totalMatches: 0, retrievalTimeMs: Date.now() - startTime, strategy: "empty_query", queryAnalysis: { intent: "general", concepts: [], complexity: "simple" } }; } const cacheKey = this.generateCacheKey(query); const cached = this.getCachedResult(cacheKey); if (cached) { logger.debug("Context retrieval cache hit"); return cached; } try { logger.info("Starting LLM-driven context retrieval", { query: query.text }); const queryAnalysis = await this.analyzeQuery(query); const strategy = this.selectStrategy(queryAnalysis, query); logger.debug("Selected retrieval strategy", { strategy: strategy.name, analysis: queryAnalysis }); const contexts = await this.executeRetrieval( query, strategy, queryAnalysis ); const rankedContexts = await this.rankAndFilter( contexts, query, queryAnalysis ); const result = { contexts: rankedContexts, totalMatches: contexts.length, retrievalTimeMs: Date.now() - startTime, strategy: strategy.name, queryAnalysis }; this.cacheResult(cacheKey, result); logger.info("Context retrieval completed", { resultsCount: rankedContexts.length, timeMs: result.retrievalTimeMs, strategy: strategy.name }); return result; } catch (error) { logger.error("Context retrieval failed:", error); return { contexts: [], totalMatches: 0, retrievalTimeMs: Date.now() - startTime, strategy: "fallback", queryAnalysis: { intent: "unknown", concepts: [], complexity: "simple" } }; } } async analyzeQuery(query) { const text = query.text.toLowerCase().trim(); const words = text.split(/\s+/); let intent = "general"; if (this.containsKeywords(text, [ "error", "exception", "fail", "bug", "issue", "problem", "debug" ])) { intent = "debug"; } else if (this.containsKeywords(text, ["how", "what", "why", "when", "where"])) { intent = "explanation"; } else if (this.containsKeywords(text, [ "implement", "create", "build", "add", "develop" ])) { intent = "implementation"; } else if (this.containsKeywords(text, [ "recent", "latest", "last", "current", "happened" ])) { intent = "recent_activity"; } const concepts = this.extractConcepts(text); let complexity = "simple"; if (words.length > 10 || concepts.length > 5) { complexity = "complex"; } else if (words.length > 5 || concepts.length > 2) { complexity = "moderate"; } return { intent, concepts, complexity }; } containsKeywords(text, keywords) { return keywords.some( (keyword) => text.toLowerCase().includes(keyword.toLowerCase()) ); } extractConcepts(text) { const technicalTerms = [ "database", "sql", "query", "index", "migration", "adapter", "frame", "event", "anchor", "digest", "context", "search", "vector", "embedding", "similarity", "score", "rank", "performance", "optimization", "cache", "pool", "connection", "error", "exception", "debug", "trace", "log", "monitor" ]; const concepts = []; const words = text.split(/\W+/).map((w) => w.toLowerCase()); for (const term of technicalTerms) { if (words.includes(term)) { concepts.push(term); } } const bigrams = this.extractBigrams(words); const technicalBigrams = [ "database adapter", "query router", "connection pool", "vector search" ]; for (const bigram of bigrams) { if (technicalBigrams.includes(bigram)) { concepts.push(bigram); } } return [...new Set(concepts)]; } extractBigrams(words) { const bigrams = []; for (let i = 0; i < words.length - 1; i++) { bigrams.push(`${words[i]} ${words[i + 1]}`); } return bigrams; } selectStrategy(analysis, query) { if (query.type) { return this.strategies.get( query.type === "keyword" ? "keyword" : query.type === "semantic" ? "semantic" : "hybrid" ) || this.strategies.get("hybrid"); } switch (analysis.intent) { case "debug": return this.strategies.get("debug"); case "recent_activity": return this.strategies.get("recent"); case "explanation": return analysis.complexity === "simple" ? this.strategies.get("keyword") : this.strategies.get("semantic"); case "implementation": return this.strategies.get("hybrid"); default: return analysis.complexity === "complex" ? this.strategies.get("semantic") : this.strategies.get("keyword"); } } async executeRetrieval(query, strategy, analysis) { const searchOptions = { query: query.text, searchType: strategy.searchType, limit: query.maxResults || 20, scoreThreshold: query.scoreThreshold || 0.1, boost: strategy.boost }; if (query.frameTypes) { searchOptions.fields = ["type", "name", "digest_text"]; } let rawResults = []; try { if (strategy.searchType === "hybrid" && strategy.weights) { const embedding = await this.generateEmbedding(query.text); rawResults = await this.adapter.searchHybrid( query.text, embedding, strategy.weights ); } else { rawResults = await this.adapter.search(searchOptions); } } catch (error) { logger.warn(`Strategy ${strategy.name} failed, trying fallback:`, error); if (strategy.fallbackStrategy) { const fallbackStrategy = this.strategies.get(strategy.fallbackStrategy); if (fallbackStrategy) { return this.executeRetrieval(query, fallbackStrategy, analysis); } } return []; } return rawResults.map((result) => ({ frame: result, score: result.score, relevanceReason: this.generateRelevanceReason(result, query, analysis), retrievalMethod: strategy.searchType, matchedFields: this.identifyMatchedFields(result, query) })); } async generateEmbedding(text) { const hash = this.simpleHash(text); return Array.from( { length: 384 }, (_, i) => (hash + i) % 100 / 100 - 0.5 ); } simpleHash(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; } return Math.abs(hash); } generateRelevanceReason(frame, query, analysis) { const reasons = []; if (frame.name.toLowerCase().includes(query.text.toLowerCase())) { reasons.push("Frame name matches query"); } if (frame.digest_text?.toLowerCase().includes(query.text.toLowerCase())) { reasons.push("Content contains query terms"); } for (const concept of analysis.concepts) { if (frame.digest_text?.toLowerCase().includes(concept.toLowerCase()) || frame.name.toLowerCase().includes(concept.toLowerCase())) { reasons.push(`Related to ${concept}`); } } if (analysis.intent === "debug" && frame.type.includes("error")) { reasons.push("Error context for debugging"); } return reasons.length > 0 ? reasons.join("; ") : "General semantic similarity"; } identifyMatchedFields(frame, query) { const matched = []; const queryLower = query.text.toLowerCase(); if (frame.name.toLowerCase().includes(queryLower)) { matched.push("name"); } if (frame.digest_text?.toLowerCase().includes(queryLower)) { matched.push("digest_text"); } if (frame.type.toLowerCase().includes(queryLower)) { matched.push("type"); } return matched; } async rankAndFilter(contexts, query, analysis) { let filtered = contexts; if (query.timeRange) { filtered = filtered.filter((ctx) => { const frameTime = new Date(ctx.frame.created_at); const start = query.timeRange?.start; const end = query.timeRange?.end; return (!start || frameTime >= start) && (!end || frameTime <= end); }); } if (query.frameTypes) { filtered = filtered.filter( (ctx) => query.frameTypes.includes(ctx.frame.type) ); } if (query.scoreThreshold) { filtered = filtered.filter((ctx) => ctx.score >= query.scoreThreshold); } const ranked = filtered.map((ctx) => ({ ...ctx, score: this.calculateEnhancedScore(ctx, query, analysis) })); ranked.sort((a, b) => b.score - a.score); const maxResults = query.maxResults || 20; return ranked.slice(0, maxResults); } calculateEnhancedScore(context, query, analysis) { let score = context.score; const ageHours = (Date.now() - context.frame.created_at) / (1e3 * 60 * 60); if (ageHours < 24) { score *= 1.2; } else if (ageHours < 168) { score *= 1.1; } if (context.frame.closed_at) { score *= 1.1; } if (analysis.intent === "debug" && context.frame.type.includes("error")) { score *= 1.5; } if (context.matchedFields.includes("name")) { score *= 1.3; } if (context.matchedFields.length > 1) { score *= 1.1; } if (analysis.intent === "recent_activity" && ageHours > 168) { score *= 0.5; } return score; } generateCacheKey(query) { return JSON.stringify({ text: query.text, type: query.type, maxResults: query.maxResults, frameTypes: query.frameTypes, scoreThreshold: query.scoreThreshold }); } getCachedResult(cacheKey) { const entry = this.queryCache.get(cacheKey); if (!entry) return null; return entry; } cacheResult(cacheKey, result) { if (this.queryCache.size >= this.cacheMaxSize) { const firstKey = this.queryCache.keys().next().value; this.queryCache.delete(firstKey); } this.queryCache.set(cacheKey, result); } // Utility methods for integration async findSimilarFrames(frameId, limit = 10) { const frame = await this.adapter.getFrame(frameId); if (!frame) { throw new Error(`Frame not found: ${frameId}`); } const query = { text: frame.digest_text || frame.name, type: "semantic", maxResults: limit, scoreThreshold: 0.3 }; const result = await this.retrieveContext(query); return result.contexts.filter((ctx) => ctx.frame.frame_id !== frameId); } async findContextForError(errorMessage, stackTrace) { const query = { text: `${errorMessage} ${stackTrace || ""}`.trim(), type: "hybrid", maxResults: 15, frameTypes: ["error", "debug", "function"], scoreThreshold: 0.2 }; const result = await this.retrieveContext(query); return result.contexts; } async getRecentContext(hours = 24, frameTypes) { const query = { text: "recent activity context", type: "keyword", maxResults: 50, timeRange: { start: new Date(Date.now() - hours * 60 * 60 * 1e3) }, frameTypes, scoreThreshold: 0.1 }; const result = await this.retrieveContext(query); return result.contexts; } // Analytics and insights getRetrievalStats() { return { cacheSize: this.queryCache.size, strategiesCount: this.strategies.size, availableStrategies: Array.from(this.strategies.keys()) }; } clearCache() { this.queryCache.clear(); logger.info("Context retrieval cache cleared"); } } export { ContextRetriever }; //# sourceMappingURL=context-retriever.js.map