UNPKG

arela

Version:

AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.

167 lines 5.67 kB
/** * RelevanceScorer - Scores memory items by relevance to query * * Scoring factors: * 1. Semantic similarity (40%) - Using simple text similarity * 2. Keyword overlap (30%) - Direct word matches * 3. Layer confidence weight (20%) - From classifier * 4. Recency (10%) - Newer items preferred for Session/Project */ export class RelevanceScorer { /** * Score multiple items by relevance to query */ score(query, items) { const queryLower = query.toLowerCase(); const queryWords = this.tokenize(queryLower); return items.map((item) => ({ ...item, score: this.calculateScore(queryLower, queryWords, item), })); } /** * Calculate relevance score for a single item */ calculateScore(queryLower, queryWords, item) { const content = this.getItemContent(item); if (!content) return 0; const contentLower = content.toLowerCase(); // 1. Semantic similarity (simple text similarity) const semanticScore = this.textSimilarity(queryLower, contentLower); // 2. Keyword overlap const keywordScore = this.keywordOverlap(queryWords, contentLower); // 3. Layer weight from classifier const layerWeight = item.layerWeight ?? 1.0; // 4. Recency score const recencyScore = this.recencyScore(item.timestamp); // Weighted combination const score = semanticScore * 0.4 + keywordScore * 0.3 + layerWeight * 0.2 + recencyScore * 0.1; return Math.max(0, Math.min(1, score)); // Clamp to [0, 1] } /** * Extract content from item (handles various formats) */ getItemContent(item) { if (typeof item.content === "string") { return item.content; } if (item.content && typeof item.content === "object") { return JSON.stringify(item.content); } return ""; } /** * Calculate text similarity using character n-grams * Returns score between 0 and 1 */ textSimilarity(text1, text2) { if (!text1 || !text2) return 0; // Use character trigrams for similarity const ngrams1 = this.getNgrams(text1, 3); const ngrams2 = this.getNgrams(text2, 3); if (ngrams1.size === 0 || ngrams2.size === 0) return 0; // Calculate Jaccard similarity const intersection = new Set([...ngrams1].filter((x) => ngrams2.has(x))); const union = new Set([...ngrams1, ...ngrams2]); return intersection.size / union.size; } /** * Generate character n-grams from text */ getNgrams(text, n) { const ngrams = new Set(); const normalized = text.trim(); for (let i = 0; i <= normalized.length - n; i++) { ngrams.add(normalized.substring(i, i + n)); } return ngrams; } /** * Calculate keyword overlap score * Returns score between 0 and 1 */ keywordOverlap(queryWords, content) { if (queryWords.length === 0) return 0; const contentWords = this.tokenize(content); const contentSet = new Set(contentWords); const matches = queryWords.filter((word) => contentSet.has(word)); return matches.length / queryWords.length; } /** * Calculate recency score based on timestamp * Returns score between 0 and 1 */ recencyScore(timestamp) { if (!timestamp) return 0.5; // Neutral if no timestamp const now = Date.now(); const age = now - timestamp; // Decay over 30 days const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days in ms const normalizedAge = Math.min(age, maxAge) / maxAge; return 1 - normalizedAge; // Newer = higher score } /** * Tokenize text into words */ tokenize(text) { return text .toLowerCase() .split(/\W+/) .filter((word) => word.length > 2); // Filter short words } /** * Calculate cosine similarity between two texts * Alternative to textSimilarity using TF-IDF-like approach */ cosineSimilarity(text1, text2) { const words1 = this.tokenize(text1); const words2 = this.tokenize(text2); if (words1.length === 0 || words2.length === 0) return 0; // Create term frequency maps const tf1 = this.termFrequency(words1); const tf2 = this.termFrequency(words2); // Get all unique terms const allTerms = new Set([...Object.keys(tf1), ...Object.keys(tf2)]); // Calculate dot product and magnitudes let dotProduct = 0; let magnitude1 = 0; let magnitude2 = 0; for (const term of allTerms) { const v1 = tf1[term] || 0; const v2 = tf2[term] || 0; dotProduct += v1 * v2; magnitude1 += v1 * v1; magnitude2 += v2 * v2; } magnitude1 = Math.sqrt(magnitude1); magnitude2 = Math.sqrt(magnitude2); if (magnitude1 === 0 || magnitude2 === 0) return 0; return dotProduct / (magnitude1 * magnitude2); } /** * Calculate term frequency */ termFrequency(words) { const tf = {}; for (const word of words) { tf[word] = (tf[word] || 0) + 1; } // Normalize by document length const total = words.length; for (const word in tf) { tf[word] = tf[word] / total; } return tf; } } //# sourceMappingURL=scorer.js.map