Add dynamicRetrieval module
Browse files- src/dynamicRetrieval.ts +53 -0
src/dynamicRetrieval.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ScoredMemory, MemoryConfig, DEFAULT_MEMORY_CONFIG, MemoryType } from './types';
|
| 2 |
+
import { applyDecayWeighting } from './memoryDecay';
|
| 3 |
+
import { formatMemoriesByType } from './typedMemory';
|
| 4 |
+
|
| 5 |
+
type QueryComplexity = 'simple' | 'moderate' | 'complex';
|
| 6 |
+
|
| 7 |
+
export function estimateQueryComplexity(query: string): QueryComplexity {
|
| 8 |
+
const wc = query.split(/\s+/).length;
|
| 9 |
+
if (wc <= 5) return 'simple';
|
| 10 |
+
if (/\b(what('s|\s+is)\s+my|am\s+i|do\s+i|where\s+do\s+i)\b/i.test(query)) return 'simple';
|
| 11 |
+
if (wc >= 15) return 'complex';
|
| 12 |
+
if (/\b(plan|schedule|organize|considering|taking\s+into\s+account|based\s+on)\b/i.test(query)) return 'complex';
|
| 13 |
+
if (/\band\b.*\band\b/i.test(query)) return 'complex';
|
| 14 |
+
return 'moderate';
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export function inferRelevantMemoryTypes(query: string): MemoryType[] {
|
| 18 |
+
const types: MemoryType[] = []; const q = query.toLowerCase();
|
| 19 |
+
if (/\b(yesterday|last|when|recently|remember\s+when|did\s+we|did\s+i|what\s+happened)\b/.test(q)) types.push('episodic');
|
| 20 |
+
if (/\b(how\s+(should|do)|what's\s+the\s+rule|format|style|remind|procedure)\b/.test(q)) types.push('procedural');
|
| 21 |
+
if (/\b(what('s|\s+is)\s+my|suggest|recommend|prefer|like|allergic|favorite|do\s+i\s+(like|have|live))\b/.test(q)) types.push('semantic');
|
| 22 |
+
return types.length ? types : ['semantic', 'episodic', 'procedural'];
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export interface RetrievalParams { topK: number; similarityThreshold: number; maxTokenBudget: number; priorityTypes: MemoryType[]; boostRecent: boolean; }
|
| 26 |
+
|
| 27 |
+
export function computeRetrievalParams(query: string, config: MemoryConfig = DEFAULT_MEMORY_CONFIG): RetrievalParams {
|
| 28 |
+
const complexity = estimateQueryComplexity(query);
|
| 29 |
+
const priorityTypes = inferRelevantMemoryTypes(query);
|
| 30 |
+
const boostRecent = /\b(today|just|now|this\s+(morning|afternoon|evening)|recently|latest)\b/i.test(query);
|
| 31 |
+
switch (complexity) {
|
| 32 |
+
case 'simple': return { topK: 3, similarityThreshold: 0.50, maxTokenBudget: 100, priorityTypes, boostRecent };
|
| 33 |
+
case 'moderate': return { topK: config.defaultTopK, similarityThreshold: config.minSimilarityThreshold, maxTokenBudget: config.maxContextTokens, priorityTypes, boostRecent };
|
| 34 |
+
case 'complex': return { topK: 12, similarityThreshold: 0.35, maxTokenBudget: 500, priorityTypes, boostRecent };
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
export function buildDynamicRetrievalQuery(embedding: number[], params: RetrievalParams) {
|
| 39 |
+
const typeFilter = params.priorityTypes.length < 3 ? `AND type IN (${params.priorityTypes.map(() => '?').join(',')})` : '';
|
| 40 |
+
const orderClause = params.boostRecent
|
| 41 |
+
? `ORDER BY (1 - vec_distance_cosine(embedding, vec_f32(?))) * (1 + 0.2 * CASE WHEN (julianday('now') - julianday(last_accessed_at / 1000, 'unixepoch')) < 1 THEN 1 ELSE 0 END) DESC`
|
| 42 |
+
: `ORDER BY vec_distance_cosine(embedding, vec_f32(?)) ASC`;
|
| 43 |
+
const sql = `SELECT id, text, embedding, type, source, created_at, last_accessed_at, access_count, importance, (1 - vec_distance_cosine(embedding, vec_f32(?))) AS similarity FROM memories WHERE (1 - vec_distance_cosine(embedding, vec_f32(?))) >= ? ${typeFilter} ${orderClause} LIMIT ?`;
|
| 44 |
+
const queryParams: unknown[] = [JSON.stringify(embedding), JSON.stringify(embedding), params.similarityThreshold, ...params.priorityTypes.length < 3 ? params.priorityTypes : [], JSON.stringify(embedding), params.topK];
|
| 45 |
+
return { sql, queryParams };
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
export function processRetrievedMemories(rawResults: Array<{ record: any; cosineSimilarity: number }>, params: RetrievalParams, config: MemoryConfig = DEFAULT_MEMORY_CONFIG) {
|
| 49 |
+
const scored = applyDecayWeighting(rawResults, config);
|
| 50 |
+
let tokenCount = 0; const selected: ScoredMemory[] = [];
|
| 51 |
+
for (const m of scored) { const t = Math.ceil(m.text.length / 4); if (tokenCount + t > params.maxTokenBudget) break; selected.push(m); tokenCount += t; }
|
| 52 |
+
return { formattedContext: formatMemoriesByType(selected.map(m => ({ text: m.text, type: m.type, source: m.source }))), memoriesUsed: selected.map(m => m.id) };
|
| 53 |
+
}
|