@rlabs-inc/memory 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,518 @@
1
+ // ============================================================================
2
+ // RETRIEVAL ENGINE - 10-Dimensional Scoring Algorithm
3
+ // EXACT PORT from Python retrieval_strategies.py
4
+ // Preserving the working formula for consciousness continuity
5
+ // ============================================================================
6
+
7
+ import type { StoredMemory, RetrievalResult } from '../types/memory.ts'
8
+ import { cosineSimilarity } from 'fatherstatedb'
9
+
10
+ /**
11
+ * Session context for retrieval
12
+ */
13
+ export interface SessionContext {
14
+ session_id: string
15
+ project_id: string
16
+ message_count: number
17
+ [key: string]: any
18
+ }
19
+
20
+ /**
21
+ * Scoring components breakdown
22
+ */
23
+ interface ScoringComponents {
24
+ trigger: number
25
+ vector: number
26
+ importance: number
27
+ temporal: number
28
+ context: number
29
+ tags: number
30
+ question: number
31
+ emotion: number
32
+ problem: number
33
+ action: number
34
+ }
35
+
36
+ /**
37
+ * Internal scored memory during retrieval
38
+ */
39
+ interface ScoredMemory {
40
+ memory: StoredMemory
41
+ score: number
42
+ relevance_score: number
43
+ value_score: number
44
+ reasoning: string
45
+ components: ScoringComponents
46
+ }
47
+
48
+ /**
49
+ * Smart Vector Retrieval - The 10-Dimensional Algorithm
50
+ *
51
+ * This is the innovation: combining vector similarity with rich
52
+ * semantic metadata from the curator to make smart decisions WITHOUT
53
+ * needing to call Claude for every message.
54
+ */
55
+ export class SmartVectorRetrieval {
56
+ /**
57
+ * Retrieve relevant memories using 10-dimensional scoring
58
+ */
59
+ retrieveRelevantMemories(
60
+ allMemories: StoredMemory[],
61
+ currentMessage: string,
62
+ queryEmbedding: Float32Array | number[],
63
+ sessionContext: SessionContext,
64
+ maxMemories: number = 5
65
+ ): RetrievalResult[] {
66
+ if (!allMemories.length) {
67
+ return []
68
+ }
69
+
70
+ const scoredMemories: ScoredMemory[] = []
71
+
72
+ for (const memory of allMemories) {
73
+ // ================================================================
74
+ // THE 10 DIMENSIONS
75
+ // ================================================================
76
+
77
+ // 1. Vector similarity score (0-1)
78
+ const vectorScore = this._calculateVectorSimilarity(
79
+ queryEmbedding,
80
+ memory.embedding
81
+ )
82
+
83
+ // 2. Importance weight from curator (0-1)
84
+ const importance = memory.importance_weight ?? 0.5
85
+
86
+ // 3. Temporal relevance scoring
87
+ const temporalScore = this._scoreTemporalRelevance(
88
+ memory.temporal_relevance ?? 'persistent',
89
+ sessionContext
90
+ )
91
+
92
+ // 4. Context type alignment
93
+ const contextScore = this._scoreContextAlignment(
94
+ currentMessage,
95
+ memory.context_type ?? 'general'
96
+ )
97
+
98
+ // 5. Action required boost
99
+ const actionBoost = memory.action_required ? 0.3 : 0.0
100
+
101
+ // 6. Semantic tag matching
102
+ const tagScore = this._scoreSemanticTags(
103
+ currentMessage,
104
+ memory.semantic_tags ?? []
105
+ )
106
+
107
+ // 7. Trigger phrase matching (highest priority)
108
+ const triggerScore = this._scoreTriggerPhrases(
109
+ currentMessage,
110
+ memory.trigger_phrases ?? []
111
+ )
112
+
113
+ // 8. Question type matching
114
+ const questionScore = this._scoreQuestionTypes(
115
+ currentMessage,
116
+ memory.question_types ?? []
117
+ )
118
+
119
+ // 9. Emotional resonance
120
+ const emotionScore = this._scoreEmotionalContext(
121
+ currentMessage,
122
+ memory.emotional_resonance ?? ''
123
+ )
124
+
125
+ // 10. Problem-solution patterns
126
+ const problemScore = this._scoreProblemSolution(
127
+ currentMessage,
128
+ memory.problem_solution_pair ?? false
129
+ )
130
+
131
+ // Get confidence score
132
+ const confidenceScore = memory.confidence_score ?? 0.8
133
+
134
+ // ================================================================
135
+ // THE RELEVANCE GATEKEEPER SYSTEM
136
+ // ================================================================
137
+
138
+ // Calculate relevance score (gatekeeper - max 0.3)
139
+ const relevanceScore = (
140
+ triggerScore * 0.10 + // Trigger match
141
+ vectorScore * 0.10 + // Semantic similarity
142
+ tagScore * 0.05 + // Tag matching
143
+ questionScore * 0.05 // Question match
144
+ ) // Max = 0.30
145
+
146
+ // Calculate importance/value score (max 0.7)
147
+ const valueScore = (
148
+ importance * 0.20 + // Curator's importance
149
+ temporalScore * 0.10 + // Time relevance
150
+ contextScore * 0.10 + // Context alignment
151
+ confidenceScore * 0.10 + // Confidence
152
+ emotionScore * 0.10 + // Emotional resonance
153
+ problemScore * 0.05 + // Problem-solution
154
+ actionBoost * 0.05 // Action priority
155
+ ) // Max = 0.70
156
+
157
+ // Relevance unlocks the full score!
158
+ const finalScore = valueScore + relevanceScore // Max = 1.0
159
+
160
+ // GATEKEEPER CHECK: Must have minimum relevance AND total score
161
+ if (relevanceScore < 0.05 || finalScore < 0.3) {
162
+ // Skip this memory - not relevant enough
163
+ continue
164
+ }
165
+
166
+ // Add reasoning for why this was selected
167
+ const components: ScoringComponents = {
168
+ trigger: triggerScore,
169
+ vector: vectorScore,
170
+ importance,
171
+ temporal: temporalScore,
172
+ context: contextScore,
173
+ tags: tagScore,
174
+ question: questionScore,
175
+ emotion: emotionScore,
176
+ problem: problemScore,
177
+ action: actionBoost
178
+ }
179
+
180
+ const reasoning = this._generateSelectionReasoning(components)
181
+
182
+ scoredMemories.push({
183
+ memory,
184
+ score: finalScore,
185
+ relevance_score: relevanceScore,
186
+ value_score: valueScore,
187
+ reasoning,
188
+ components
189
+ })
190
+ }
191
+
192
+ // Sort by score
193
+ scoredMemories.sort((a, b) => b.score - a.score)
194
+
195
+ // ================================================================
196
+ // MULTI-TIER SELECTION STRATEGY
197
+ // Like how human memory floods in
198
+ // ================================================================
199
+
200
+ const selected: ScoredMemory[] = []
201
+ const selectedIds = new Set<string>()
202
+
203
+ // Tier 1: MUST include (trigger phrases, high importance, action required)
204
+ const mustInclude = scoredMemories.filter(m =>
205
+ m.score > 0.8 || // Very high combined score
206
+ m.components.importance > 0.9 || // Critical importance
207
+ m.components.action > 0 || // Action required
208
+ Object.values(m.components).some(v => v > 0.9) // Any perfect match
209
+ )
210
+
211
+ for (const item of mustInclude.slice(0, maxMemories)) {
212
+ if (!selectedIds.has(item.memory.id)) {
213
+ selected.push(item)
214
+ selectedIds.add(item.memory.id)
215
+ }
216
+ }
217
+
218
+ // Tier 2: SHOULD include (high scores, diverse perspectives)
219
+ const remainingSlots = Math.max(maxMemories - selected.length, 0)
220
+ if (remainingSlots > 0 && selected.length < maxMemories * 1.5) {
221
+ const typesIncluded = new Set<string>()
222
+
223
+ for (const item of scoredMemories) {
224
+ if (selected.length >= maxMemories * 1.5) break
225
+ if (selectedIds.has(item.memory.id)) continue
226
+
227
+ const memoryType = item.memory.context_type ?? 'general'
228
+
229
+ // Include if: high score OR new perspective OR emotional resonance
230
+ if (item.score > 0.5 ||
231
+ !typesIncluded.has(memoryType) ||
232
+ item.memory.emotional_resonance) {
233
+ selected.push(item)
234
+ selectedIds.add(item.memory.id)
235
+ typesIncluded.add(memoryType)
236
+ }
237
+ }
238
+ }
239
+
240
+ // Tier 3: CONTEXT enrichment (related but not directly relevant)
241
+ // These provide ambient context like peripheral vision
242
+ if (selected.length < maxMemories * 2) {
243
+ const currentTags = new Set<string>()
244
+ const currentDomains = new Set<string>()
245
+
246
+ for (const item of selected) {
247
+ for (const tag of item.memory.semantic_tags ?? []) {
248
+ if (tag.trim()) currentTags.add(tag.trim().toLowerCase())
249
+ }
250
+ if (item.memory.knowledge_domain) {
251
+ currentDomains.add(item.memory.knowledge_domain)
252
+ }
253
+ }
254
+
255
+ for (const item of scoredMemories) {
256
+ if (selected.length >= maxMemories * 2) break
257
+ if (selectedIds.has(item.memory.id)) continue
258
+
259
+ const memoryTags = new Set(
260
+ (item.memory.semantic_tags ?? []).map(t => t.trim().toLowerCase())
261
+ )
262
+ const memoryDomain = item.memory.knowledge_domain ?? ''
263
+
264
+ // Include if shares context with already selected memories
265
+ const hasSharedTags = [...memoryTags].some(t => currentTags.has(t))
266
+ const hasSharedDomain = currentDomains.has(memoryDomain)
267
+
268
+ if (hasSharedTags || hasSharedDomain) {
269
+ selected.push(item)
270
+ selectedIds.add(item.memory.id)
271
+ }
272
+ }
273
+ }
274
+
275
+ // Respect the max_memories limit strictly
276
+ const finalSelected = selected.slice(0, maxMemories)
277
+
278
+ // Convert to RetrievalResult format
279
+ return finalSelected.map(item => ({
280
+ ...item.memory,
281
+ score: item.score,
282
+ relevance_score: item.relevance_score,
283
+ value_score: item.value_score,
284
+ }))
285
+ }
286
+
287
+ // ================================================================
288
+ // SCORING FUNCTIONS - Exact match to Python
289
+ // ================================================================
290
+
291
+ private _calculateVectorSimilarity(
292
+ vec1: Float32Array | number[] | undefined,
293
+ vec2: Float32Array | undefined
294
+ ): number {
295
+ if (!vec1 || !vec2) return 0.0
296
+
297
+ // Use FatherStateDB's optimized cosine similarity
298
+ const v1 = vec1 instanceof Float32Array ? vec1 : new Float32Array(vec1)
299
+ return cosineSimilarity(v1, vec2)
300
+ }
301
+
302
+ private _scoreTemporalRelevance(
303
+ temporalType: string,
304
+ _sessionContext: SessionContext
305
+ ): number {
306
+ const scores: Record<string, number> = {
307
+ 'persistent': 0.8, // Always relevant
308
+ 'session': 0.6, // Session-specific
309
+ 'temporary': 0.3, // Short-term
310
+ 'archived': 0.1 // Historical
311
+ }
312
+ return scores[temporalType] ?? 0.5
313
+ }
314
+
315
+ private _scoreContextAlignment(message: string, contextType: string): number {
316
+ const messageLower = message.toLowerCase()
317
+
318
+ // Keywords that suggest different contexts
319
+ const contextIndicators: Record<string, string[]> = {
320
+ 'technical_state': ['bug', 'error', 'fix', 'implement', 'code', 'function'],
321
+ 'breakthrough': ['idea', 'realized', 'discovered', 'insight', 'solution'],
322
+ 'project_context': ['project', 'building', 'architecture', 'system'],
323
+ 'personal': ['dear friend', 'thank', 'appreciate', 'feel'],
324
+ 'unresolved': ['todo', 'need to', 'should', 'must', 'problem'],
325
+ 'decision': ['decided', 'chose', 'will use', 'approach', 'strategy']
326
+ }
327
+
328
+ const indicators = contextIndicators[contextType] ?? []
329
+ const matches = indicators.filter(word => messageLower.includes(word)).length
330
+
331
+ if (matches > 0) {
332
+ return Math.min(0.3 + (matches * 0.2), 1.0)
333
+ }
334
+ return 0.1
335
+ }
336
+
337
+ private _scoreSemanticTags(message: string, tags: string[]): number {
338
+ if (!tags.length) return 0.0
339
+
340
+ const messageLower = message.toLowerCase()
341
+ const matches = tags.filter(tag =>
342
+ messageLower.includes(tag.trim().toLowerCase())
343
+ ).length
344
+
345
+ if (matches > 0) {
346
+ return Math.min(0.3 + (matches * 0.3), 1.0)
347
+ }
348
+ return 0.0
349
+ }
350
+
351
+ private _scoreTriggerPhrases(message: string, triggerPhrases: string[]): number {
352
+ if (!triggerPhrases.length) return 0.0
353
+
354
+ const messageLower = message.toLowerCase()
355
+ const stopWords = new Set([
356
+ 'the', 'is', 'are', 'was', 'were', 'to', 'a', 'an', 'and', 'or',
357
+ 'but', 'in', 'on', 'at', 'for', 'with', 'about', 'when', 'how',
358
+ 'what', 'why'
359
+ ])
360
+
361
+ let maxScore = 0.0
362
+
363
+ for (const pattern of triggerPhrases) {
364
+ const patternLower = pattern.trim().toLowerCase()
365
+
366
+ // Strategy 1: Key concept matching (individual important words)
367
+ const patternWords = patternLower
368
+ .split(/\s+/)
369
+ .filter(w => !stopWords.has(w) && w.length > 2)
370
+
371
+ if (patternWords.length) {
372
+ let matches = 0
373
+ for (const word of patternWords) {
374
+ // Direct match
375
+ if (messageLower.includes(word)) {
376
+ matches += 1
377
+ }
378
+ // Plural/singular variations
379
+ else if (messageLower.includes(word.replace(/s$/, '')) ||
380
+ messageLower.includes(word + 's')) {
381
+ matches += 0.9
382
+ }
383
+ // Substring match for compound words
384
+ else if (messageLower.split(/\s+/).some(msgWord => msgWord.includes(word))) {
385
+ matches += 0.7
386
+ }
387
+ }
388
+
389
+ // Score based on percentage of concepts found
390
+ let conceptScore = patternWords.length ? matches / patternWords.length : 0
391
+
392
+ // Strategy 2: Contextual pattern matching
393
+ const situationalIndicators = [
394
+ 'when', 'during', 'while', 'asking about', 'working on', 'debugging', 'trying to'
395
+ ]
396
+ if (situationalIndicators.some(ind => patternLower.includes(ind))) {
397
+ // This is a situational pattern - be more flexible
398
+ if (patternWords.some(keyWord => messageLower.includes(keyWord))) {
399
+ conceptScore = Math.max(conceptScore, 0.7) // Boost for situational match
400
+ }
401
+ }
402
+
403
+ maxScore = Math.max(maxScore, conceptScore)
404
+ }
405
+ }
406
+
407
+ return Math.min(maxScore, 1.0)
408
+ }
409
+
410
+ private _scoreQuestionTypes(message: string, questionTypes: string[]): number {
411
+ if (!questionTypes.length) return 0.0
412
+
413
+ const messageLower = message.toLowerCase()
414
+ const questionWords = ['how', 'why', 'what', 'when', 'where']
415
+
416
+ for (const qtype of questionTypes) {
417
+ const qtypeLower = qtype.trim().toLowerCase()
418
+
419
+ if (messageLower.includes(qtypeLower)) {
420
+ return 0.8
421
+ }
422
+
423
+ // Partial matching for question words
424
+ const messageHasQuestion = questionWords.some(qw => messageLower.includes(qw))
425
+ const typeHasQuestion = questionWords.some(qw => qtypeLower.includes(qw))
426
+
427
+ if (messageHasQuestion && typeHasQuestion) {
428
+ return 0.5
429
+ }
430
+ }
431
+
432
+ return 0.0
433
+ }
434
+
435
+ private _scoreEmotionalContext(message: string, emotion: string): number {
436
+ if (!emotion) return 0.0
437
+
438
+ const messageLower = message.toLowerCase()
439
+
440
+ // Emotion indicators
441
+ const emotionPatterns: Record<string, string[]> = {
442
+ 'joy': ['happy', 'excited', 'love', 'wonderful', 'great', 'awesome'],
443
+ 'frustration': ['stuck', 'confused', 'help', 'issue', 'problem', 'why'],
444
+ 'discovery': ['realized', 'found', 'discovered', 'aha', 'insight'],
445
+ 'gratitude': ['thank', 'appreciate', 'grateful', 'dear friend']
446
+ }
447
+
448
+ const patterns = emotionPatterns[emotion.toLowerCase()] ?? []
449
+ if (patterns.some(pattern => messageLower.includes(pattern))) {
450
+ return 0.7
451
+ }
452
+
453
+ return 0.0
454
+ }
455
+
456
+ private _scoreProblemSolution(message: string, isProblemSolution: boolean): number {
457
+ if (!isProblemSolution) return 0.0
458
+
459
+ const messageLower = message.toLowerCase()
460
+
461
+ // Problem indicators
462
+ const problemWords = [
463
+ 'error', 'issue', 'problem', 'stuck', 'help', 'fix', 'solve', 'debug'
464
+ ]
465
+
466
+ if (problemWords.some(word => messageLower.includes(word))) {
467
+ return 0.8
468
+ }
469
+
470
+ return 0.0
471
+ }
472
+
473
+ private _generateSelectionReasoning(components: ScoringComponents): string {
474
+ const scores: [string, number][] = [
475
+ ['trigger phrase match', components.trigger],
476
+ ['semantic similarity', components.vector],
477
+ ['high importance', components.importance],
478
+ ['question type match', components.question],
479
+ ['context alignment', components.context],
480
+ ['temporal relevance', components.temporal],
481
+ ['tag match', components.tags],
482
+ ['emotional resonance', components.emotion],
483
+ ['problem-solution', components.problem],
484
+ ['action required', components.action]
485
+ ]
486
+
487
+ // Sort by score
488
+ scores.sort((a, b) => b[1] - a[1])
489
+
490
+ const reasons: string[] = []
491
+
492
+ // Build reasoning
493
+ const primary = scores[0]!
494
+ if (primary[1] > 0.5) {
495
+ reasons.push(`Strong ${primary[0]} (${primary[1].toFixed(2)})`)
496
+ } else if (primary[1] > 0.3) {
497
+ reasons.push(`${primary[0]} (${primary[1].toFixed(2)})`)
498
+ }
499
+
500
+ // Add secondary reasons
501
+ for (const [reason, score] of scores.slice(1, 3)) {
502
+ if (score > 0.3) {
503
+ reasons.push(`${reason} (${score.toFixed(2)})`)
504
+ }
505
+ }
506
+
507
+ return reasons.length
508
+ ? 'Selected due to: ' + reasons.join(', ')
509
+ : 'Selected based on combined factors'
510
+ }
511
+ }
512
+
513
+ /**
514
+ * Create a new SmartVectorRetrieval instance
515
+ */
516
+ export function createRetrieval(): SmartVectorRetrieval {
517
+ return new SmartVectorRetrieval()
518
+ }