memories-lite 0.10.1 → 0.99.2
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.
- package/dist/config/defaults.js +13 -20
- package/dist/config/manager.js +3 -10
- package/dist/memory/index.d.ts +12 -3
- package/dist/memory/index.js +61 -155
- package/dist/memory/memory.types.d.ts +1 -2
- package/dist/prompts/index.d.ts +53 -17
- package/dist/prompts/index.js +63 -23
- package/dist/types/index.d.ts +40 -234
- package/dist/types/index.js +4 -27
- package/dist/vectorstores/lite.d.ts +1 -0
- package/dist/vectorstores/lite.js +11 -6
- package/memories-lite-a42ac5108869b599bcbac21069f63fb47f07452fcc4b87e89b3c06a945612d0b.db +0 -0
- package/memories-lite-a9137698d8d3fdbf27efcdc8cd372084b52d484e8db866c5455bbb3f85299b54.db +0 -0
- package/package.json +1 -1
- package/src/config/defaults.ts +13 -20
- package/src/config/manager.ts +3 -10
- package/src/memory/index.ts +76 -197
- package/src/memory/memory.types.ts +1 -2
- package/src/prompts/index.ts +69 -26
- package/src/types/index.ts +8 -46
- package/src/vectorstores/lite.ts +13 -7
- package/tests/init.mem.ts +7 -4
- package/tests/lite.spec.ts +44 -34
- package/tests/memory.discussion.search.test.ts +377 -0
- package/tests/memory.discussion.test.ts +279 -0
- package/tests/memory.update.test.ts +5 -1
- package/tests/memory.facts.test.ts +0 -175
- package/tests/memory.todo.test.ts +0 -126
package/src/prompts/index.ts
CHANGED
|
@@ -1,36 +1,83 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { MemoryItem } from "../types";
|
|
3
3
|
|
|
4
|
-
//
|
|
4
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
// DISCUSSION SYNTHESIS - Nouveau système de mémoire
|
|
6
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Schema pour la synthèse de discussion
|
|
10
|
+
* Produit un titre court et une synthèse opérationnelle
|
|
11
|
+
*/
|
|
12
|
+
export const DiscussionSynthesisSchema = z.object({
|
|
13
|
+
title: z.string().describe("Titre court et descriptif (6-10 mots)"),
|
|
14
|
+
summary: z.string().describe("Synthèse opérationnelle des points clés (50-100 mots)")
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Prompt par défaut pour la synthèse de discussion
|
|
19
|
+
* Peut être remplacé via capturePrompt dans AddMemoryOptions
|
|
20
|
+
*/
|
|
21
|
+
export const DEFAULT_DISCUSSION_PROMPT = `Tu es un expert en synthèse opérationnelle.
|
|
22
|
+
|
|
23
|
+
À partir de cette discussion, génère :
|
|
24
|
+
1. TITRE: Un titre court et descriptif (10-20 mots) qui capture l'essence de la demande
|
|
25
|
+
2. SUMMARY: Les points clés du chemin de résolution en markdown (max 150 mots)
|
|
26
|
+
|
|
27
|
+
Cette synthèse servira à retrouver et réappliquer ce pattern de résolution similaire.
|
|
28
|
+
Utilise la même langue que la discussion.
|
|
29
|
+
|
|
30
|
+
Discussion à synthétiser:
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Génère les messages pour la synthèse de discussion
|
|
35
|
+
* @param discussion - Contenu de la discussion formatée
|
|
36
|
+
* @param capturePrompt - Prompt custom optionnel (remplace DEFAULT_DISCUSSION_PROMPT)
|
|
37
|
+
* @returns [systemPrompt, userPrompt]
|
|
38
|
+
*/
|
|
39
|
+
export function getDiscussionSynthesisMessages(
|
|
40
|
+
discussion: string,
|
|
41
|
+
capturePrompt?: string
|
|
42
|
+
): [string, string] {
|
|
43
|
+
const systemPrompt = capturePrompt || DEFAULT_DISCUSSION_PROMPT;
|
|
44
|
+
return [systemPrompt, discussion];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
48
|
+
// @deprecated - Ancien système de capture (todo, factual)
|
|
49
|
+
// Ces exports sont conservés pour compatibilité mais ne doivent plus être utilisés
|
|
50
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @deprecated Use DiscussionSynthesisSchema instead
|
|
54
|
+
*/
|
|
5
55
|
export const FactRetrievalSchema_simple = z.object({
|
|
6
56
|
facts: z
|
|
7
57
|
.array(z.string())
|
|
8
58
|
.describe("An array of distinct facts extracted from the conversation."),
|
|
9
59
|
});
|
|
10
60
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
//
|
|
61
|
+
/**
|
|
62
|
+
* @deprecated Use DiscussionSynthesisSchema instead
|
|
63
|
+
* Types todo et factual supprimés - seul assistant_preference reste pour compatibilité
|
|
64
|
+
*/
|
|
16
65
|
export const FactRetrievalSchema_extended = z.object({
|
|
17
66
|
facts: z
|
|
18
67
|
.array(
|
|
19
68
|
z.object({
|
|
20
69
|
fact: z.string().describe("The fact extracted from the conversation."),
|
|
21
70
|
existing: z.boolean().describe("Whether the fact is already present"),
|
|
22
|
-
type: z.enum(["assistant_preference"
|
|
23
|
-
.describe(`The type of the fact.
|
|
24
|
-
Use 'assistant_preference' for Assistant behavior preferences (style/language/constraints/commands).
|
|
25
|
-
Use 'factual' for stable user facts (identity, preferences, beliefs, work context).
|
|
26
|
-
Use 'todo' ONLY if the user explicitly asks to save/keep as a todo (e.g., « garde/enregistre en todo », « ajoute un todo »). Do not infer todos.
|
|
27
|
-
`),
|
|
71
|
+
type: z.enum(["assistant_preference"])
|
|
72
|
+
.describe(`The type of the fact. Only 'assistant_preference' is supported.`),
|
|
28
73
|
})
|
|
29
74
|
)
|
|
30
75
|
});
|
|
31
76
|
|
|
32
77
|
|
|
33
|
-
|
|
78
|
+
/**
|
|
79
|
+
* @deprecated Memory updates are disabled - use capture() for new memories
|
|
80
|
+
*/
|
|
34
81
|
export const MemoryUpdateSchema = z.object({
|
|
35
82
|
memory: z
|
|
36
83
|
.array(
|
|
@@ -54,21 +101,14 @@ export const MemoryUpdateSchema = z.object({
|
|
|
54
101
|
"The reason why you selected this event.",
|
|
55
102
|
),
|
|
56
103
|
type: z
|
|
57
|
-
.enum(["
|
|
58
|
-
.describe("Type of the memory.
|
|
104
|
+
.enum(["assistant_preference"])
|
|
105
|
+
.describe("Type of the memory. Only 'assistant_preference' is supported."),
|
|
59
106
|
}),
|
|
60
107
|
)
|
|
61
108
|
.describe(
|
|
62
109
|
"An array representing the state of memory items after processing new facts.",
|
|
63
110
|
),
|
|
64
111
|
});
|
|
65
|
-
/**
|
|
66
|
-
* Practical Application:
|
|
67
|
-
*
|
|
68
|
-
* If the task is "factual" (e.g., "Where do I live?", "What's my job?") → retrieve factual memory.
|
|
69
|
-
* If the task is about assistant behavior (e.g., "How should I respond?") → retrieve assistant_preference memory.
|
|
70
|
-
* If the task is a user task/reminder (e.g., "Add a reminder to call the bank tomorrow") → retrieve todo memory.
|
|
71
|
-
*/
|
|
72
112
|
export const MEMORY_STRING_SYSTEM = `# DIRECTIVES FOR MEMORIES
|
|
73
113
|
- Information stored in memory is always enclosed within the <memories> tag.
|
|
74
114
|
- Prioritize the latest user message over memories (the user's current question is authoritative).
|
|
@@ -80,10 +120,10 @@ export const MEMORY_STRING_SYSTEM = `# DIRECTIVES FOR MEMORIES
|
|
|
80
120
|
|
|
81
121
|
export const MEMORY_STRING_PREFIX = "Use these contextual memories to guide your response. Prioritize the user's question. Ignore irrelevant memories."
|
|
82
122
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
123
|
+
/**
|
|
124
|
+
* @deprecated Use getDiscussionSynthesisMessages instead
|
|
125
|
+
* Cette fonction est conservée pour compatibilité avec l'ancien système
|
|
126
|
+
*/
|
|
87
127
|
export function getFactRetrievalMessages(
|
|
88
128
|
parsedMessages: string,
|
|
89
129
|
customRules: string = "",
|
|
@@ -132,6 +172,9 @@ Remember the following:
|
|
|
132
172
|
return [systemPrompt, userPrompt];
|
|
133
173
|
}
|
|
134
174
|
|
|
175
|
+
/**
|
|
176
|
+
* @deprecated Memory updates are disabled by config
|
|
177
|
+
*/
|
|
135
178
|
export function getUpdateMemoryMessages(
|
|
136
179
|
retrievedOldMemory: Array<{ id: string; text: string }>,
|
|
137
180
|
newRetrievedFacts: any[],
|
package/src/types/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ export interface VectorStoreConfig {
|
|
|
30
30
|
cacheTtl?: number;
|
|
31
31
|
scoring?: MemoryScoringConfig;
|
|
32
32
|
recencyCleanupThreshold?: number;
|
|
33
|
+
searchThreshold?: number; // Seuil minimum de score pour retourner un résultat (0-1)
|
|
33
34
|
[key: string]: any;
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -51,19 +52,6 @@ export interface LLMConfig {
|
|
|
51
52
|
modelProperties?: Record<string, any>;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
export interface Neo4jConfig {
|
|
55
|
-
url: string;
|
|
56
|
-
username: string;
|
|
57
|
-
password: string;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface GraphStoreConfig {
|
|
61
|
-
provider: string;
|
|
62
|
-
config: Neo4jConfig;
|
|
63
|
-
llm?: LLMConfig;
|
|
64
|
-
customPrompt?: string;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
55
|
export interface MemoryTypeConfig {
|
|
68
56
|
alpha: number; // Weight for cosine similarity
|
|
69
57
|
beta: number; // Weight for recency
|
|
@@ -72,9 +60,8 @@ export interface MemoryTypeConfig {
|
|
|
72
60
|
}
|
|
73
61
|
|
|
74
62
|
export interface MemoryScoringConfig {
|
|
75
|
-
todo: MemoryTypeConfig;
|
|
76
|
-
factual: MemoryTypeConfig;
|
|
77
63
|
assistant_preference: MemoryTypeConfig;
|
|
64
|
+
discussion: MemoryTypeConfig;
|
|
78
65
|
default: MemoryTypeConfig; // Fallback if type is missing or unknown
|
|
79
66
|
}
|
|
80
67
|
|
|
@@ -96,12 +83,10 @@ export interface MemoryConfig {
|
|
|
96
83
|
historyStore?: HistoryStoreConfig;
|
|
97
84
|
disableHistory?: boolean;
|
|
98
85
|
historyDbPath?: string;
|
|
99
|
-
|
|
100
|
-
graphStore?: GraphStoreConfig;
|
|
101
|
-
enableGraph?: boolean;
|
|
86
|
+
capturePrompt?: string; // Prompt par défaut pour la synthèse de discussion
|
|
102
87
|
}
|
|
103
88
|
|
|
104
|
-
export type MemoryType = '
|
|
89
|
+
export type MemoryType = 'assistant_preference' | 'discussion';
|
|
105
90
|
|
|
106
91
|
export interface MemoryItem {
|
|
107
92
|
id: string;
|
|
@@ -162,19 +147,13 @@ export const MemoryConfigSchema = z.object({
|
|
|
162
147
|
dimension: z.number().optional(),
|
|
163
148
|
client: z.any().optional(),
|
|
164
149
|
scoring: z.object({
|
|
165
|
-
|
|
166
|
-
alpha: z.number(),
|
|
167
|
-
beta: z.number(),
|
|
168
|
-
gamma: z.number(),
|
|
169
|
-
halfLifeDays: z.number(),
|
|
170
|
-
}),
|
|
171
|
-
factual: z.object({
|
|
150
|
+
assistant_preference: z.object({
|
|
172
151
|
alpha: z.number(),
|
|
173
152
|
beta: z.number(),
|
|
174
153
|
gamma: z.number(),
|
|
175
154
|
halfLifeDays: z.number(),
|
|
176
155
|
}),
|
|
177
|
-
|
|
156
|
+
discussion: z.object({
|
|
178
157
|
alpha: z.number(),
|
|
179
158
|
beta: z.number(),
|
|
180
159
|
gamma: z.number(),
|
|
@@ -188,6 +167,7 @@ export const MemoryConfigSchema = z.object({
|
|
|
188
167
|
}),
|
|
189
168
|
}).optional(),
|
|
190
169
|
recencyCleanupThreshold: z.number().min(0).max(1).optional(),
|
|
170
|
+
searchThreshold: z.number().min(0).max(1).optional(),
|
|
191
171
|
})
|
|
192
172
|
.passthrough(),
|
|
193
173
|
}),
|
|
@@ -200,25 +180,7 @@ export const MemoryConfigSchema = z.object({
|
|
|
200
180
|
}),
|
|
201
181
|
}),
|
|
202
182
|
historyDbPath: z.string().optional(),
|
|
203
|
-
|
|
204
|
-
enableGraph: z.boolean().optional(),
|
|
205
|
-
graphStore: z
|
|
206
|
-
.object({
|
|
207
|
-
provider: z.string(),
|
|
208
|
-
config: z.object({
|
|
209
|
-
url: z.string(),
|
|
210
|
-
username: z.string(),
|
|
211
|
-
password: z.string(),
|
|
212
|
-
}),
|
|
213
|
-
llm: z
|
|
214
|
-
.object({
|
|
215
|
-
provider: z.string(),
|
|
216
|
-
config: z.record(z.string(), z.any()),
|
|
217
|
-
})
|
|
218
|
-
.optional(),
|
|
219
|
-
customPrompt: z.string().optional(),
|
|
220
|
-
})
|
|
221
|
-
.optional(),
|
|
183
|
+
capturePrompt: z.string().optional(),
|
|
222
184
|
historyStore: z
|
|
223
185
|
.object({
|
|
224
186
|
provider: z.string(),
|
package/src/vectorstores/lite.ts
CHANGED
|
@@ -31,7 +31,8 @@ export class LiteVectorStore implements VectorStore {
|
|
|
31
31
|
private dimension: number;
|
|
32
32
|
private currentUserId: string;
|
|
33
33
|
private scoringConfig: MemoryScoringConfig;
|
|
34
|
-
private cleanupThreshold?: number; //
|
|
34
|
+
private cleanupThreshold?: number; // Seuil de recency pour cleanup
|
|
35
|
+
private searchThreshold: number; // Seuil minimum de score pour retourner un résultat
|
|
35
36
|
private static cache: Map<string, LiteVectorStore>;
|
|
36
37
|
constructor(config: VectorStoreConfig, currentUserId: string) {
|
|
37
38
|
if (!config.scoring) {
|
|
@@ -41,7 +42,8 @@ export class LiteVectorStore implements VectorStore {
|
|
|
41
42
|
this.currentUserId = currentUserId;
|
|
42
43
|
this.isSecure = config.secure || false;
|
|
43
44
|
this.scoringConfig = config.scoring;
|
|
44
|
-
this.cleanupThreshold = config.recencyCleanupThreshold || 0.25; // (default 0.25 means 2 times the half-life
|
|
45
|
+
this.cleanupThreshold = config.recencyCleanupThreshold || 0.25; // (default 0.25 means 2 times the half-life)
|
|
46
|
+
this.searchThreshold = config.searchThreshold ?? 0; // Seuil de score (default 0 = pas de filtrage)
|
|
45
47
|
config.rootPath = config.rootPath || process.cwd();
|
|
46
48
|
const filename = this.isSecure ? `memories-lite-${currentUserId}.db` : `memories-lite-global.db`;
|
|
47
49
|
this.dbPath = (config.rootPath == ':memory:') ? ':memory:' : path.join(config.rootPath, filename);
|
|
@@ -246,11 +248,15 @@ export class LiteVectorStore implements VectorStore {
|
|
|
246
248
|
const cosineScore = this.cosineSimilarity(query, vector);
|
|
247
249
|
const hybridScore = this.calculateHybridScore(cosineScore, payload);
|
|
248
250
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
251
|
+
//
|
|
252
|
+
// Filtrer par searchThreshold - ne retourner que les résultats au-dessus du seuil
|
|
253
|
+
if (hybridScore >= this.searchThreshold) {
|
|
254
|
+
results.push({
|
|
255
|
+
id: memoryVector.id,
|
|
256
|
+
payload: memoryVector.payload,
|
|
257
|
+
score: hybridScore,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
254
260
|
}
|
|
255
261
|
}
|
|
256
262
|
|
package/tests/init.mem.ts
CHANGED
|
@@ -6,9 +6,10 @@ dotenv.config();
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Helper to initialize MemoriesLite instance and generate a random userId.
|
|
9
|
-
* @param
|
|
9
|
+
* @param capturePrompt Optional prompt to inject into the memory config.
|
|
10
|
+
* @param searchThreshold Optional minimum score threshold for search results (0-1)
|
|
10
11
|
*/
|
|
11
|
-
export function createTestMemory({
|
|
12
|
+
export function createTestMemory({capturePrompt, dimension, rootPath, secure, searchThreshold}:any) {
|
|
12
13
|
dimension = dimension || 768;
|
|
13
14
|
const userId =
|
|
14
15
|
Math.random().toString(36).substring(2, 15) +
|
|
@@ -17,7 +18,7 @@ export function createTestMemory({customPrompt, dimension, rootPath, secure}:any
|
|
|
17
18
|
const memory = new MemoriesLite({
|
|
18
19
|
version: "v1.1",
|
|
19
20
|
disableHistory: true,
|
|
20
|
-
...(
|
|
21
|
+
...(capturePrompt ? { capturePrompt } : {}),
|
|
21
22
|
embedder: {
|
|
22
23
|
provider: "openai",
|
|
23
24
|
config: { dimension, apiKey: process.env.OPENAI_API_KEY!, model: "text-embedding-3-small" }
|
|
@@ -27,7 +28,9 @@ export function createTestMemory({customPrompt, dimension, rootPath, secure}:any
|
|
|
27
28
|
config: {
|
|
28
29
|
dimension,
|
|
29
30
|
rootPath: (rootPath || ":memory:"),
|
|
30
|
-
secure: secure || false
|
|
31
|
+
secure: secure || false,
|
|
32
|
+
searchThreshold: searchThreshold ?? 0 // Par défaut 0 (pas de filtrage) pour les tests existants
|
|
33
|
+
}
|
|
31
34
|
},
|
|
32
35
|
llm: {
|
|
33
36
|
provider: "openai",
|
package/tests/lite.spec.ts
CHANGED
|
@@ -25,6 +25,10 @@ jest.mock('sqlite3', () => {
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Tests for LiteVectorStore private methods
|
|
30
|
+
* Updated to use new memory types: assistant_preference and discussion
|
|
31
|
+
*/
|
|
28
32
|
describe('LiteVectorStore Private Methods', () => {
|
|
29
33
|
let store: LiteVectorStore;
|
|
30
34
|
const userId = 'test-user';
|
|
@@ -98,59 +102,65 @@ describe('LiteVectorStore Private Methods', () => {
|
|
|
98
102
|
const veryOldDate = new Date(now.getTime() - 400 * 24 * 60 * 60 * 1000).toISOString();
|
|
99
103
|
const scoring = DEFAULT_MEMORY_CONFIG.vectorStore.config.scoring!;
|
|
100
104
|
|
|
101
|
-
it('should prioritize cosine similarity for
|
|
102
|
-
|
|
105
|
+
it('should prioritize cosine similarity for assistant_preference (high alpha)', () => {
|
|
106
|
+
//
|
|
107
|
+
// assistant_preference: { alpha: 0.60, beta: 0.05, gamma: 0.35, halfLifeDays: Infinity }
|
|
108
|
+
const payload: MemoryPayload = { memoryId: 'mem-ap1', userId: userId, type: 'assistant_preference', createdAt: twoDaysAgo };
|
|
103
109
|
const cosineScore = 0.9;
|
|
104
|
-
const expectedRecency =
|
|
105
|
-
const expectedScore = scoring.
|
|
110
|
+
const expectedRecency = 1.0; // Infinity halfLife → recency = 1
|
|
111
|
+
const expectedScore = scoring.assistant_preference.alpha * cosineScore + scoring.assistant_preference.beta * expectedRecency + scoring.assistant_preference.gamma;
|
|
106
112
|
const hybridScore = (store as any).calculateHybridScore(cosineScore, payload);
|
|
107
113
|
expect(hybridScore).toBeCloseTo(expectedScore, 5);
|
|
108
|
-
|
|
114
|
+
// alpha * cosine (0.54) > beta * recency (0.05)
|
|
115
|
+
expect(scoring.assistant_preference.alpha * cosineScore).toBeGreaterThan(scoring.assistant_preference.beta * expectedRecency);
|
|
109
116
|
});
|
|
110
117
|
|
|
111
|
-
it('should
|
|
112
|
-
|
|
118
|
+
it('should return constant score for discussion (beta=1, alpha=0, gamma=0)', () => {
|
|
119
|
+
//
|
|
120
|
+
// discussion: { alpha: 0, beta: 1, gamma: 0, halfLifeDays: Infinity }
|
|
121
|
+
// Score = 0 * cosine + 1 * recency + 0 = 1 (constant)
|
|
122
|
+
const payload: MemoryPayload = { memoryId: 'mem-d1', userId: userId, type: 'discussion', createdAt: twoDaysAgo };
|
|
113
123
|
const cosineScore = 0.5;
|
|
114
|
-
const expectedRecency =
|
|
115
|
-
const expectedScore = scoring.
|
|
124
|
+
const expectedRecency = 1.0; // Infinity halfLife → recency = 1
|
|
125
|
+
const expectedScore = scoring.discussion.alpha * cosineScore + scoring.discussion.beta * expectedRecency + scoring.discussion.gamma;
|
|
116
126
|
const hybridScore = (store as any).calculateHybridScore(cosineScore, payload);
|
|
117
127
|
expect(hybridScore).toBeCloseTo(expectedScore, 5);
|
|
118
|
-
expect(
|
|
128
|
+
expect(hybridScore).toBe(1); // Score constant = 1
|
|
119
129
|
});
|
|
120
130
|
|
|
121
|
-
it('should have
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
it('should have constant score for old discussion memory (no decay)', () => {
|
|
132
|
+
//
|
|
133
|
+
// discussion with Infinity halfLife → old memories have same score as new
|
|
134
|
+
const payload: MemoryPayload = { memoryId: 'mem-d2', userId: userId, type: 'discussion', createdAt: veryOldDate };
|
|
135
|
+
const cosineScore = 0.9;
|
|
136
|
+
const expectedRecency = 1.0; // Infinity halfLife → recency = 1 even for old dates
|
|
137
|
+
const hybridScore = (store as any).calculateHybridScore(cosineScore, payload);
|
|
138
|
+
expect(hybridScore).toBe(1); // Score constant = 1
|
|
129
139
|
});
|
|
130
140
|
|
|
131
141
|
it('should handle assistant_preference with no decay (Infinity half-life)', () => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
142
|
+
const payload: MemoryPayload = { memoryId: 'mem-a1', userId: userId, type: 'assistant_preference', createdAt: veryOldDate };
|
|
143
|
+
const cosineScore = 0.7;
|
|
144
|
+
const expectedRecency = 1.0;
|
|
145
|
+
const expectedScore = scoring.assistant_preference.alpha * cosineScore + scoring.assistant_preference.beta * expectedRecency + scoring.assistant_preference.gamma;
|
|
146
|
+
const hybridScore = (store as any).calculateHybridScore(cosineScore, payload);
|
|
147
|
+
expect(hybridScore).toBeCloseTo(expectedScore, 5);
|
|
138
148
|
});
|
|
139
149
|
|
|
140
150
|
it('should use default scoring if type is missing', () => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
151
|
+
const payload: MemoryPayload = { memoryId: 'mem-def1', userId: userId, createdAt: sevenDaysAgo }; // No type specified
|
|
152
|
+
const cosineScore = 0.8;
|
|
153
|
+
const expectedRecency = (store as any).calculateRecencyScore(sevenDaysAgo, scoring.default.halfLifeDays);
|
|
154
|
+
const expectedScore = scoring.default.alpha * cosineScore + scoring.default.beta * expectedRecency + scoring.default.gamma;
|
|
155
|
+
const hybridScore = (store as any).calculateHybridScore(cosineScore, payload);
|
|
156
|
+
expect(hybridScore).toBeCloseTo(expectedScore, 5);
|
|
147
157
|
});
|
|
148
158
|
|
|
149
159
|
it('should return score >= 0 even with negative cosine similarity', () => {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
160
|
+
const payload: MemoryPayload = { memoryId: 'mem-neg1', userId: userId, type: 'assistant_preference', createdAt: twoDaysAgo };
|
|
161
|
+
const cosineScore = -0.5;
|
|
162
|
+
const hybridScore = (store as any).calculateHybridScore(cosineScore, payload);
|
|
163
|
+
expect(hybridScore).toBeGreaterThanOrEqual(0);
|
|
154
164
|
});
|
|
155
165
|
|
|
156
166
|
});
|