@xdarkicex/openclaw-memory-libravdb 1.3.19 → 1.3.21
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/README.md +1 -1
- package/docs/README.md +1 -1
- package/docs/architecture.md +8 -14
- package/docs/implementation.md +2 -2
- package/docs/mathematics-v2.md +485 -0
- package/package.json +1 -1
- package/src/context-engine.ts +50 -7
- package/src/memory-provider.ts +19 -81
- package/src/openclaw-plugin-sdk.d.ts +6 -1
- package/src/scoring.ts +93 -1
- package/src/sidecar.ts +31 -1
- package/src/temporal.ts +385 -0
- package/src/tokens.ts +16 -0
- package/src/types.ts +9 -0
package/src/temporal.ts
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import type { SearchResult } from "./types.js";
|
|
2
|
+
|
|
3
|
+
const TEMPORAL_PATTERN_WEIGHTS: Array<{ label: string; weight: number; patterns: RegExp[] }> = [
|
|
4
|
+
{
|
|
5
|
+
label: "how many days",
|
|
6
|
+
weight: 1.0,
|
|
7
|
+
patterns: [/\bhow\s+many\s+days\b/i],
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
label: "how long",
|
|
11
|
+
weight: 0.9,
|
|
12
|
+
patterns: [/\bhow\s+long\b/i],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
label: "before or after",
|
|
16
|
+
weight: 0.8,
|
|
17
|
+
patterns: [/\bbefore\b/i, /\bafter\b/i],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
label: "since or between",
|
|
21
|
+
weight: 0.7,
|
|
22
|
+
patterns: [/\bsince\b/i, /\bbetween\b/i],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
label: "first or earlier",
|
|
26
|
+
weight: 0.8,
|
|
27
|
+
patterns: [/\bfirst\b/i, /\bearlier\b/i, /\bwhich\s+came\s+first\b/i],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
label: "when did",
|
|
31
|
+
weight: 0.7,
|
|
32
|
+
patterns: [/\bwhen\s+did\b/i],
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const TEMPORAL_ANCHOR_PATTERNS: RegExp[] = [
|
|
37
|
+
/\b\d{4}-\d{2}-\d{2}\b/g,
|
|
38
|
+
/\b\d{1,2}\/\d{1,2}(?:\/\d{2,4})?\b/g,
|
|
39
|
+
/\b(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:t(?:ember)?)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\s+\d{1,2}(?:st|nd|rd|th)?(?:,\s*\d{4})?\b/gi,
|
|
40
|
+
/\b(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/gi,
|
|
41
|
+
/\b(?:today|yesterday|tomorrow|last\s+(?:week|month|year|night|saturday|sunday)|next\s+(?:week|month|year|monday|tuesday|wednesday|thursday|friday|saturday|sunday)|mid-?[a-z]+)\b/gi,
|
|
42
|
+
/\b\d{1,2}:\d{2}(?:\s?[ap]m)?\b/gi,
|
|
43
|
+
/\b\d{10,13}\b/g,
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const TEMPORAL_XI_NORM = 1.5;
|
|
47
|
+
const TEMPORAL_XI_THRESHOLD = 0.3;
|
|
48
|
+
const TEMPORAL_ANCHOR_NORM = 3;
|
|
49
|
+
const TEMPORAL_ANCHOR_CACHE_MAX = 4096;
|
|
50
|
+
const temporalAnchorCache = new Map<string, number>();
|
|
51
|
+
|
|
52
|
+
const TEMPORAL_SLOT_STOPWORDS = new Set([
|
|
53
|
+
"the",
|
|
54
|
+
"and",
|
|
55
|
+
"for",
|
|
56
|
+
"with",
|
|
57
|
+
"that",
|
|
58
|
+
"this",
|
|
59
|
+
"have",
|
|
60
|
+
"from",
|
|
61
|
+
"your",
|
|
62
|
+
"what",
|
|
63
|
+
"when",
|
|
64
|
+
"where",
|
|
65
|
+
"which",
|
|
66
|
+
"would",
|
|
67
|
+
"could",
|
|
68
|
+
"should",
|
|
69
|
+
"about",
|
|
70
|
+
"into",
|
|
71
|
+
"some",
|
|
72
|
+
"them",
|
|
73
|
+
"they",
|
|
74
|
+
"been",
|
|
75
|
+
"just",
|
|
76
|
+
"want",
|
|
77
|
+
"looking",
|
|
78
|
+
"look",
|
|
79
|
+
"help",
|
|
80
|
+
"need",
|
|
81
|
+
"recommend",
|
|
82
|
+
"suggestions",
|
|
83
|
+
"suggest",
|
|
84
|
+
"advice",
|
|
85
|
+
"think",
|
|
86
|
+
"also",
|
|
87
|
+
"did",
|
|
88
|
+
"does",
|
|
89
|
+
"do",
|
|
90
|
+
"after",
|
|
91
|
+
"before",
|
|
92
|
+
"since",
|
|
93
|
+
"between",
|
|
94
|
+
"first",
|
|
95
|
+
"earlier",
|
|
96
|
+
"many",
|
|
97
|
+
"days",
|
|
98
|
+
"long",
|
|
99
|
+
"how",
|
|
100
|
+
"did",
|
|
101
|
+
"take",
|
|
102
|
+
"took",
|
|
103
|
+
"it",
|
|
104
|
+
"me",
|
|
105
|
+
"my",
|
|
106
|
+
"i",
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
export interface TemporalQuerySignal {
|
|
110
|
+
indicator: number;
|
|
111
|
+
active: boolean;
|
|
112
|
+
matchedPatterns: string[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface TemporalRecoveryDebugCandidate {
|
|
116
|
+
id: string;
|
|
117
|
+
text: string;
|
|
118
|
+
selected: boolean;
|
|
119
|
+
temporalAnchorDensity: number;
|
|
120
|
+
semanticScore: number;
|
|
121
|
+
recencyScore: number;
|
|
122
|
+
slotCoverage: number;
|
|
123
|
+
slotMatches: string[];
|
|
124
|
+
finalScore: number;
|
|
125
|
+
rationale: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface TemporalRecoveryRankingResult {
|
|
129
|
+
ranked: SearchResult[];
|
|
130
|
+
debug: TemporalRecoveryDebugCandidate[];
|
|
131
|
+
temporalQuery: TemporalQuerySignal;
|
|
132
|
+
slots: string[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function detectTemporalQuerySignal(queryText: string): TemporalQuerySignal {
|
|
136
|
+
const matchedPatterns: string[] = [];
|
|
137
|
+
let weightedMatches = 0;
|
|
138
|
+
|
|
139
|
+
for (const entry of TEMPORAL_PATTERN_WEIGHTS) {
|
|
140
|
+
if (entry.patterns.some((pattern) => pattern.test(queryText))) {
|
|
141
|
+
matchedPatterns.push(entry.label);
|
|
142
|
+
weightedMatches += entry.weight;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const indicator = clamp01(weightedMatches / TEMPORAL_XI_NORM);
|
|
147
|
+
return {
|
|
148
|
+
indicator,
|
|
149
|
+
active: indicator >= TEMPORAL_XI_THRESHOLD,
|
|
150
|
+
matchedPatterns,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function getTemporalAnchorDensity(docKey: string, text: string): number {
|
|
155
|
+
const cacheKey = `${docKey}\n${text}`;
|
|
156
|
+
const cached = temporalAnchorCache.get(cacheKey);
|
|
157
|
+
if (typeof cached === "number") {
|
|
158
|
+
touchTemporalAnchorCache(cacheKey, cached);
|
|
159
|
+
return cached;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const uniqueMatches = new Set<string>();
|
|
163
|
+
for (const pattern of TEMPORAL_ANCHOR_PATTERNS) {
|
|
164
|
+
for (const match of text.matchAll(pattern)) {
|
|
165
|
+
const value = match[0]?.trim().toLowerCase();
|
|
166
|
+
if (value) {
|
|
167
|
+
uniqueMatches.add(value);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const density = clamp01(uniqueMatches.size / TEMPORAL_ANCHOR_NORM);
|
|
173
|
+
touchTemporalAnchorCache(cacheKey, density);
|
|
174
|
+
return density;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function rankTemporalRecoveryCandidates(
|
|
178
|
+
items: SearchResult[],
|
|
179
|
+
opts: {
|
|
180
|
+
queryText: string;
|
|
181
|
+
maxSelected?: number;
|
|
182
|
+
nowMs?: number;
|
|
183
|
+
recencyLambda?: number;
|
|
184
|
+
},
|
|
185
|
+
): TemporalRecoveryRankingResult {
|
|
186
|
+
const temporalQuery = detectTemporalQuerySignal(opts.queryText);
|
|
187
|
+
const slots = extractTemporalSlots(opts.queryText);
|
|
188
|
+
const recencyLambda = Math.max(0, opts.recencyLambda ?? 0.00001);
|
|
189
|
+
const now = opts.nowMs ?? Date.now();
|
|
190
|
+
const maxSelected = Math.max(1, Math.floor(opts.maxSelected ?? 3));
|
|
191
|
+
|
|
192
|
+
const decorated = items.map((item) => {
|
|
193
|
+
const semanticScore = clamp01(typeof item.finalScore === "number" ? item.finalScore : item.score ?? 0);
|
|
194
|
+
const recencyScore = computeRecencyScore(item, now, recencyLambda);
|
|
195
|
+
const temporalAnchorDensity = getTemporalAnchorDensity(
|
|
196
|
+
`${typeof item.metadata.collection === "string" ? item.metadata.collection : "unknown"}::${item.id}`,
|
|
197
|
+
item.text,
|
|
198
|
+
);
|
|
199
|
+
const { coverage, matches } = computeSlotCoverage(slots, item.text);
|
|
200
|
+
const finalScore = clamp01(
|
|
201
|
+
(0.40 * semanticScore) +
|
|
202
|
+
(0.25 * recencyScore) +
|
|
203
|
+
(0.20 * temporalAnchorDensity) +
|
|
204
|
+
(0.15 * coverage) +
|
|
205
|
+
(temporalQuery.active ? 0.05 : 0),
|
|
206
|
+
);
|
|
207
|
+
return {
|
|
208
|
+
item,
|
|
209
|
+
semanticScore,
|
|
210
|
+
recencyScore,
|
|
211
|
+
temporalAnchorDensity,
|
|
212
|
+
slotCoverage: coverage,
|
|
213
|
+
slotMatches: matches,
|
|
214
|
+
finalScore,
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const selectedIDs = new Set<string>();
|
|
219
|
+
const coveredSlots = new Set<string>();
|
|
220
|
+
const selected: SearchResult[] = [];
|
|
221
|
+
|
|
222
|
+
for (let pass = 0; pass < maxSelected; pass += 1) {
|
|
223
|
+
let best: (typeof decorated)[number] | null = null;
|
|
224
|
+
let bestScore = Number.NEGATIVE_INFINITY;
|
|
225
|
+
|
|
226
|
+
for (const candidate of decorated) {
|
|
227
|
+
if (selectedIDs.has(candidate.item.id)) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const marginalCoverage = candidate.slotMatches.filter((slot) => !coveredSlots.has(slot)).length / Math.max(1, slots.length);
|
|
231
|
+
const combined = candidate.finalScore + (0.25 * marginalCoverage);
|
|
232
|
+
if (combined > bestScore) {
|
|
233
|
+
best = candidate;
|
|
234
|
+
bestScore = combined;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!best || bestScore < 0.12) {
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
selectedIDs.add(best.item.id);
|
|
243
|
+
for (const slot of best.slotMatches) {
|
|
244
|
+
coveredSlots.add(slot);
|
|
245
|
+
}
|
|
246
|
+
selected.push({
|
|
247
|
+
...best.item,
|
|
248
|
+
finalScore: best.finalScore,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const remaining = decorated
|
|
253
|
+
.filter((candidate) => !selectedIDs.has(candidate.item.id))
|
|
254
|
+
.sort((left, right) => right.finalScore - left.finalScore)
|
|
255
|
+
.map((candidate) => ({
|
|
256
|
+
...candidate.item,
|
|
257
|
+
finalScore: candidate.finalScore,
|
|
258
|
+
}));
|
|
259
|
+
|
|
260
|
+
const ranked = [...selected, ...remaining];
|
|
261
|
+
const debug = decorated
|
|
262
|
+
.sort((left, right) => right.finalScore - left.finalScore)
|
|
263
|
+
.map((candidate) => ({
|
|
264
|
+
id: candidate.item.id,
|
|
265
|
+
text: candidate.item.text,
|
|
266
|
+
selected: selectedIDs.has(candidate.item.id),
|
|
267
|
+
temporalAnchorDensity: candidate.temporalAnchorDensity,
|
|
268
|
+
semanticScore: candidate.semanticScore,
|
|
269
|
+
recencyScore: candidate.recencyScore,
|
|
270
|
+
slotCoverage: candidate.slotCoverage,
|
|
271
|
+
slotMatches: candidate.slotMatches,
|
|
272
|
+
finalScore: candidate.finalScore,
|
|
273
|
+
rationale: buildTemporalRecoveryRationale(candidate.slotCoverage, candidate.temporalAnchorDensity, candidate.semanticScore),
|
|
274
|
+
}));
|
|
275
|
+
|
|
276
|
+
return { ranked, debug, temporalQuery, slots };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function resetTemporalCachesForTest(): void {
|
|
280
|
+
temporalAnchorCache.clear();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function extractTemporalSlots(text: string): string[] {
|
|
284
|
+
const clauses = text
|
|
285
|
+
.split(/(?:\bafter\b|\bbefore\b|\bbetween\b|\bor\b|\band\b|\bthen\b|[?.!,;]+)/i)
|
|
286
|
+
.map((part) => part.trim())
|
|
287
|
+
.filter((part) => part.length > 0);
|
|
288
|
+
const slots = new Set<string>();
|
|
289
|
+
|
|
290
|
+
for (const clause of clauses) {
|
|
291
|
+
const terms = normalizeTerms(clause)
|
|
292
|
+
.filter((term) => term.length >= 3 && !TEMPORAL_SLOT_STOPWORDS.has(term));
|
|
293
|
+
if (terms.length === 0) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (terms.length <= 3) {
|
|
297
|
+
slots.add(terms.join(" "));
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
slots.add(terms.slice(0, 4).join(" "));
|
|
301
|
+
slots.add(terms.slice(-4).join(" "));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (slots.size === 0) {
|
|
305
|
+
const fallback = normalizeTerms(text).filter((term) => term.length >= 3 && !TEMPORAL_SLOT_STOPWORDS.has(term));
|
|
306
|
+
if (fallback.length > 0) {
|
|
307
|
+
slots.add(fallback.slice(0, 4).join(" "));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return [...slots].slice(0, 4);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function computeSlotCoverage(slots: string[], candidateText: string): { coverage: number; matches: string[] } {
|
|
315
|
+
if (slots.length === 0) {
|
|
316
|
+
return { coverage: 0, matches: [] };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const candidateTerms = new Set(normalizeTerms(candidateText));
|
|
320
|
+
const matches: string[] = [];
|
|
321
|
+
let covered = 0;
|
|
322
|
+
|
|
323
|
+
for (const slot of slots) {
|
|
324
|
+
const slotTerms = normalizeTerms(slot).filter((term) => term.length >= 3);
|
|
325
|
+
if (slotTerms.length === 0) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const overlap = slotTerms.filter((term) => candidateTerms.has(term)).length / slotTerms.length;
|
|
329
|
+
if (overlap >= 0.5) {
|
|
330
|
+
covered += 1;
|
|
331
|
+
matches.push(slot);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
coverage: covered / Math.max(1, slots.length),
|
|
337
|
+
matches,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function buildTemporalRecoveryRationale(slotCoverage: number, anchorDensity: number, semanticScore: number): string {
|
|
342
|
+
if (slotCoverage >= 0.5 && anchorDensity >= 0.5) {
|
|
343
|
+
return "slot coverage and temporal anchors both supported this candidate";
|
|
344
|
+
}
|
|
345
|
+
if (slotCoverage >= 0.5) {
|
|
346
|
+
return "slot coverage lifted this candidate toward the query's subevents";
|
|
347
|
+
}
|
|
348
|
+
if (anchorDensity >= 0.5) {
|
|
349
|
+
return "temporal anchors lifted this candidate toward the query's date logic";
|
|
350
|
+
}
|
|
351
|
+
if (semanticScore >= 0.6) {
|
|
352
|
+
return "semantic similarity kept this candidate in the temporal pool";
|
|
353
|
+
}
|
|
354
|
+
return "candidate remained in the bounded temporal recovery pool";
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function computeRecencyScore(item: SearchResult, now: number, recencyLambda: number): number {
|
|
358
|
+
const ts = typeof item.metadata.ts === "number" ? item.metadata.ts : now;
|
|
359
|
+
const ageSeconds = Math.max(0, now - ts) / 1000;
|
|
360
|
+
return Math.exp(-recencyLambda * ageSeconds);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function normalizeTerms(text: string): string[] {
|
|
364
|
+
return text
|
|
365
|
+
.toLowerCase()
|
|
366
|
+
.split(/[^a-z0-9_]+/i)
|
|
367
|
+
.filter((term) => term.length > 0);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function touchTemporalAnchorCache(cacheKey: string, value: number): void {
|
|
371
|
+
if (temporalAnchorCache.has(cacheKey)) {
|
|
372
|
+
temporalAnchorCache.delete(cacheKey);
|
|
373
|
+
}
|
|
374
|
+
temporalAnchorCache.set(cacheKey, value);
|
|
375
|
+
if (temporalAnchorCache.size > TEMPORAL_ANCHOR_CACHE_MAX) {
|
|
376
|
+
const oldestKey = temporalAnchorCache.keys().next().value;
|
|
377
|
+
if (typeof oldestKey === "string") {
|
|
378
|
+
temporalAnchorCache.delete(oldestKey);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function clamp01(value: number): number {
|
|
384
|
+
return Math.min(1, Math.max(0, value));
|
|
385
|
+
}
|
package/src/tokens.ts
CHANGED
|
@@ -21,6 +21,22 @@ export function fitPromptBudget(items: SearchResult[], budget: number): SearchRe
|
|
|
21
21
|
return selected;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export function fitPromptBudgetFirstFit(items: SearchResult[], budget: number): SearchResult[] {
|
|
25
|
+
const selected: SearchResult[] = [];
|
|
26
|
+
let used = 0;
|
|
27
|
+
|
|
28
|
+
for (const item of items) {
|
|
29
|
+
const cost = estimateTokens(item.text);
|
|
30
|
+
if (used + cost > budget) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
selected.push(item);
|
|
34
|
+
used += cost;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return selected;
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
export function countTokens(messages: Array<{ content: string }>): number {
|
|
25
41
|
return messages.reduce((sum, msg) => sum + estimateTokens(msg.content), 0);
|
|
26
42
|
}
|
package/src/types.ts
CHANGED
|
@@ -203,12 +203,21 @@ export interface ContextAssembleResult {
|
|
|
203
203
|
id: string;
|
|
204
204
|
text: string;
|
|
205
205
|
selected: boolean;
|
|
206
|
+
tokenEstimate: number;
|
|
207
|
+
temporalAnchorDensity: number;
|
|
206
208
|
semanticScore: number;
|
|
209
|
+
slotCoverage?: number;
|
|
210
|
+
slotMatches?: string[];
|
|
207
211
|
lexicalCoverage: number;
|
|
208
212
|
recencyScore: number;
|
|
209
213
|
finalScore: number;
|
|
210
214
|
rationale: string;
|
|
211
215
|
}>;
|
|
216
|
+
recoveryReserveTokens?: number;
|
|
217
|
+
temporalQueryIndicator?: number;
|
|
218
|
+
temporalQueryActive?: boolean;
|
|
219
|
+
temporalQueryPatterns?: string[];
|
|
220
|
+
temporalRecoverySlots?: string[];
|
|
212
221
|
};
|
|
213
222
|
}
|
|
214
223
|
|