@melandlabs/memory-store 1.2.5 → 1.3.1
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 +47 -16
- package/dist/{config-Dw4aXj2a.d.ts → config-CbE2t4SW.d.ts} +172 -17
- package/dist/http.d.ts +2 -1
- package/dist/http.js +594 -270
- package/dist/index.d.ts +10 -6
- package/dist/index.js +316 -99
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.js +615 -250
- package/dist/search/derive.d.ts +2 -1
- package/dist/search/distill.d.ts +2 -1
- package/dist/search/unified-search.d.ts +3 -2
- package/dist/search/unified-search.js +316 -99
- package/dist/server/cli-http.js +966 -320
- package/dist/server/cli-mcp.js +987 -300
- package/dist/server/cli-shared.d.ts +20 -7
- package/dist/server/cli-shared.js +370 -48
- package/dist/storage/postgres-raw-message-factory.d.ts +2 -1
- package/dist/storage/raw-message-store.d.ts +20 -2
- package/dist/storage/sqlite-raw-message-store.d.ts +8 -1
- package/dist/storage/sqlite-vector-index.d.ts +2 -1
- package/dist/storage/sqlite-vector-index.js +1 -1
- package/dist/{unified-search-DfE8ek7W.d.ts → unified-search-2YQ6EGuQ.d.ts} +17 -6
- package/package.json +15 -11
package/dist/server/cli-mcp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { isFactType, vsaNormalizeVector, vsaAssertSameDim } from '@melandlabs/contracts';
|
|
3
|
-
import
|
|
4
|
-
import { ChromaVectorStore } from '@melandlabs/rag';
|
|
3
|
+
import '@melandlabs/ai/memory';
|
|
4
|
+
import { ChromaVectorStore as ChromaVectorStore$1 } from '@melandlabs/rag';
|
|
5
5
|
import { mkdirSync } from 'fs';
|
|
6
6
|
import { dirname } from 'path';
|
|
7
7
|
import { getOpenContextPath } from '@melandlabs/env-config';
|
|
@@ -13,6 +13,8 @@ import { applyReflectedConsolidationPlan } from '@melandlabs/memory-consolidatio
|
|
|
13
13
|
import { isDerivedKind } from '@melandlabs/contracts/derived-fact';
|
|
14
14
|
import { isEntityKind } from '@melandlabs/contracts/entity-edge';
|
|
15
15
|
import { superpose, bind, unbind, cleanup, cosineSimilarity } from '@melandlabs/vsa';
|
|
16
|
+
import { createHash } from 'crypto';
|
|
17
|
+
import { ChromaVectorStore } from '@melandlabs/rag/chroma-vector-store';
|
|
16
18
|
|
|
17
19
|
var __defProp = Object.defineProperty;
|
|
18
20
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -23,27 +25,6 @@ var __export = (target, all) => {
|
|
|
23
25
|
for (var name in all)
|
|
24
26
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
25
27
|
};
|
|
26
|
-
function normalizeTimestampToMs(value) {
|
|
27
|
-
if (!Number.isFinite(value)) {
|
|
28
|
-
return 0;
|
|
29
|
-
}
|
|
30
|
-
if (value < 1e11) {
|
|
31
|
-
return Math.floor(value * 1e3);
|
|
32
|
-
}
|
|
33
|
-
return Math.floor(value);
|
|
34
|
-
}
|
|
35
|
-
function rawMessageToMemoryRecord(message) {
|
|
36
|
-
return {
|
|
37
|
-
id: message.messageId,
|
|
38
|
-
userId: message.userId,
|
|
39
|
-
timestamp: normalizeTimestampToMs(message.timestamp),
|
|
40
|
-
text: message.archivedAt ? void 0 : message.content,
|
|
41
|
-
mediaRefs: message.attachments?.map((item) => item.url).filter(Boolean),
|
|
42
|
-
embedding: message.embedding,
|
|
43
|
-
embeddingModel: message.embeddingModel,
|
|
44
|
-
embeddingContentHash: message.embeddingContentHash
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
28
|
var init_contracts = __esm({
|
|
48
29
|
"src/contracts.ts"() {
|
|
49
30
|
}
|
|
@@ -64,66 +45,14 @@ function getRawMessagesCollectionName() {
|
|
|
64
45
|
return process.env.CHROMA_RAW_MESSAGES_COLLECTION || process.env.CHROMA_MEMORY_COLLECTION || DEFAULT_RAW_MESSAGES_COLLECTION;
|
|
65
46
|
}
|
|
66
47
|
function getChromaStore(collectionName) {
|
|
67
|
-
return new ChromaVectorStore({
|
|
48
|
+
return new ChromaVectorStore$1({
|
|
68
49
|
url: process.env.CHROMA_URL,
|
|
69
50
|
collectionName
|
|
70
51
|
});
|
|
71
52
|
}
|
|
72
|
-
function normalizeTimestampToMs2(value) {
|
|
73
|
-
if (!Number.isFinite(value)) {
|
|
74
|
-
return 0;
|
|
75
|
-
}
|
|
76
|
-
if (value < 1e11) {
|
|
77
|
-
return Math.floor(value * 1e3);
|
|
78
|
-
}
|
|
79
|
-
return Math.floor(value);
|
|
80
|
-
}
|
|
81
53
|
function toBooleanValue(value) {
|
|
82
54
|
return value === true || value === "true" || value === 1 || value === "1";
|
|
83
55
|
}
|
|
84
|
-
function rawMessageToChunk(message) {
|
|
85
|
-
if (!message.embedding || message.embedding.length === 0) {
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
const record = rawMessageToMemoryRecord(message);
|
|
89
|
-
const document = buildMemoryRecordEmbeddingDocument(record);
|
|
90
|
-
if (!document.content) {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
return {
|
|
94
|
-
id: message.messageId,
|
|
95
|
-
documentId: message.messageId,
|
|
96
|
-
content: document.content,
|
|
97
|
-
embedding: message.embedding,
|
|
98
|
-
metadata: {
|
|
99
|
-
sourceType: "raw_message",
|
|
100
|
-
userId: message.userId,
|
|
101
|
-
platform: message.platform,
|
|
102
|
-
botId: message.botId,
|
|
103
|
-
channel: message.channel,
|
|
104
|
-
person: message.person,
|
|
105
|
-
timestamp: normalizeTimestampToMs2(message.timestamp),
|
|
106
|
-
memoryStage: message.memoryStage,
|
|
107
|
-
embeddingModel: message.embeddingModel,
|
|
108
|
-
embeddingDimensions: message.embeddingDimensions,
|
|
109
|
-
contentHash: message.embeddingContentHash,
|
|
110
|
-
archived: Boolean(message.archivedAt),
|
|
111
|
-
role: typeof message.metadata?.role === "string" ? message.metadata.role : void 0
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
async function upsertRawMessagesToChroma(messages) {
|
|
116
|
-
if (!isRawMessageChromaEnabled() || messages.length === 0) {
|
|
117
|
-
return 0;
|
|
118
|
-
}
|
|
119
|
-
const chunks = messages.map(rawMessageToChunk).filter((chunk) => chunk !== null);
|
|
120
|
-
if (chunks.length === 0) {
|
|
121
|
-
return 0;
|
|
122
|
-
}
|
|
123
|
-
const store = getChromaStore(getRawMessagesCollectionName());
|
|
124
|
-
await store.addChunks(chunks);
|
|
125
|
-
return chunks.length;
|
|
126
|
-
}
|
|
127
56
|
async function searchRawMessagesWithChroma(input) {
|
|
128
57
|
if (!isRawMessageChromaEnabled() || input.queryEmbedding.length === 0) {
|
|
129
58
|
return [];
|
|
@@ -325,59 +254,73 @@ var init_raw_message_store = __esm({
|
|
|
325
254
|
}
|
|
326
255
|
});
|
|
327
256
|
|
|
328
|
-
// src/
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
257
|
+
// src/search/unified-search.ts
|
|
258
|
+
init_chroma_memory_index();
|
|
259
|
+
init_raw_message_store();
|
|
260
|
+
|
|
261
|
+
// src/search/applicability.ts
|
|
262
|
+
var APPLICABILITY_SCOPES = /* @__PURE__ */ new Set([
|
|
263
|
+
"global",
|
|
264
|
+
"task",
|
|
265
|
+
"conversation",
|
|
266
|
+
"channel",
|
|
267
|
+
"project",
|
|
268
|
+
"custom"
|
|
269
|
+
]);
|
|
270
|
+
function isRecord(value) {
|
|
271
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
272
|
+
}
|
|
273
|
+
function invalidRuntimeContext(message) {
|
|
274
|
+
throw new TypeError(`memory-store search runtime context: ${message}`);
|
|
275
|
+
}
|
|
276
|
+
function validateApplicabilityContext(value, index) {
|
|
277
|
+
const path = `applicabilityContexts[${index}]`;
|
|
278
|
+
if (!isRecord(value)) {
|
|
279
|
+
invalidRuntimeContext(`${path} must be an object`);
|
|
332
280
|
}
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
281
|
+
const scope = value.scope;
|
|
282
|
+
if (typeof scope !== "string" || !APPLICABILITY_SCOPES.has(scope)) {
|
|
283
|
+
invalidRuntimeContext(`${path}.scope is not supported`);
|
|
284
|
+
}
|
|
285
|
+
if (value.key !== void 0 && typeof value.key !== "string") {
|
|
286
|
+
invalidRuntimeContext(`${path}.key must be a string when provided`);
|
|
287
|
+
}
|
|
288
|
+
if (scope !== "global" && (typeof value.key !== "string" || value.key.trim().length === 0)) {
|
|
289
|
+
invalidRuntimeContext(`${path}.key must be non-empty for scope "${scope}"`);
|
|
290
|
+
}
|
|
291
|
+
for (const field of ["validFrom", "validUntil"]) {
|
|
292
|
+
const boundary = value[field];
|
|
293
|
+
if (boundary !== void 0 && (typeof boundary !== "number" || !Number.isFinite(boundary))) {
|
|
294
|
+
invalidRuntimeContext(`${path}.${field} must be a finite epoch-millisecond number`);
|
|
342
295
|
}
|
|
343
|
-
const vector = await deps.embedQuery({
|
|
344
|
-
userId: message.userId,
|
|
345
|
-
query: message.content
|
|
346
|
-
});
|
|
347
|
-
out.push({
|
|
348
|
-
...message,
|
|
349
|
-
embedding: vector,
|
|
350
|
-
embeddingModel: message.embeddingModel ?? "server",
|
|
351
|
-
embeddingDimensions: vector.length,
|
|
352
|
-
embeddingUpdatedAt: Date.now()
|
|
353
|
-
});
|
|
354
296
|
}
|
|
355
|
-
return out;
|
|
356
297
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
298
|
+
function resolveSearchRuntimeContext(input, runtimeContext, now = Date.now) {
|
|
299
|
+
if (runtimeContext === void 0) {
|
|
300
|
+
return void 0;
|
|
301
|
+
}
|
|
302
|
+
if (!isRecord(runtimeContext) || !Array.isArray(runtimeContext.applicabilityContexts)) {
|
|
303
|
+
invalidRuntimeContext("applicabilityContexts must be an array");
|
|
304
|
+
}
|
|
305
|
+
for (const [index, context] of runtimeContext.applicabilityContexts.entries()) {
|
|
306
|
+
validateApplicabilityContext(context, index);
|
|
307
|
+
}
|
|
308
|
+
let applicabilityAt;
|
|
309
|
+
if (input.asOf === void 0) {
|
|
310
|
+
applicabilityAt = now();
|
|
311
|
+
} else {
|
|
312
|
+
const parsed = typeof input.asOf === "string" ? Date.parse(input.asOf) : Number.NaN;
|
|
313
|
+
if (!Number.isFinite(parsed)) {
|
|
314
|
+
invalidRuntimeContext("asOf must be a parseable timestamp when a runtime context is supplied");
|
|
315
|
+
}
|
|
316
|
+
applicabilityAt = parsed;
|
|
373
317
|
}
|
|
374
|
-
return {
|
|
318
|
+
return {
|
|
319
|
+
applicabilityContexts: runtimeContext.applicabilityContexts,
|
|
320
|
+
applicabilityAt
|
|
321
|
+
};
|
|
375
322
|
}
|
|
376
323
|
|
|
377
|
-
// src/search/unified-search.ts
|
|
378
|
-
init_chroma_memory_index();
|
|
379
|
-
init_raw_message_store();
|
|
380
|
-
|
|
381
324
|
// src/search/reranker.ts
|
|
382
325
|
async function applyReranker(reranker, query, results) {
|
|
383
326
|
if (!reranker || results.length === 0) {
|
|
@@ -571,6 +514,7 @@ function materializeSignals(lists, hit) {
|
|
|
571
514
|
function listNameToChannel(name) {
|
|
572
515
|
if (name === "memory-semantic") return "semantic";
|
|
573
516
|
if (name === "memory-bm25" || name === "memory-lexical") return "lexical";
|
|
517
|
+
if (name === "memory-hybrid") return "hybrid";
|
|
574
518
|
if (name === "memory-entity") return "entity";
|
|
575
519
|
return void 0;
|
|
576
520
|
}
|
|
@@ -628,7 +572,6 @@ async function resolveScopePeer(input) {
|
|
|
628
572
|
|
|
629
573
|
// src/search/gather-evidence.ts
|
|
630
574
|
var DEFAULT_TIERS = ["summary", "raw", "insight", "knowledge"];
|
|
631
|
-
var MAX_EVIDENCE_CHARS = 600;
|
|
632
575
|
function toSummaryResult(row) {
|
|
633
576
|
const start = typeof row.startTimestamp === "number" ? row.startTimestamp : void 0;
|
|
634
577
|
const end = typeof row.endTimestamp === "number" ? row.endTimestamp : void 0;
|
|
@@ -646,7 +589,7 @@ function toSummaryResult(row) {
|
|
|
646
589
|
}
|
|
647
590
|
};
|
|
648
591
|
}
|
|
649
|
-
async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers) {
|
|
592
|
+
async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers, runtimeContext) {
|
|
650
593
|
if (typeof deps.searchSummaries !== "function") {
|
|
651
594
|
return { hits: [], warnings: [] };
|
|
652
595
|
}
|
|
@@ -659,7 +602,8 @@ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers)
|
|
|
659
602
|
limit,
|
|
660
603
|
threshold,
|
|
661
604
|
authToken: input.authToken,
|
|
662
|
-
...peerPeers.length > 0 ? { peers: peerPeers } : {}
|
|
605
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
606
|
+
...runtimeContext ?? {}
|
|
663
607
|
});
|
|
664
608
|
const hits = rows.map(toSummaryResult);
|
|
665
609
|
return { hits, warnings: [] };
|
|
@@ -677,12 +621,6 @@ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers)
|
|
|
677
621
|
};
|
|
678
622
|
}
|
|
679
623
|
}
|
|
680
|
-
function truncate(input, max = MAX_EVIDENCE_CHARS) {
|
|
681
|
-
if (input.length <= max) {
|
|
682
|
-
return input;
|
|
683
|
-
}
|
|
684
|
-
return `${input.slice(0, max - 1)}\u2026`;
|
|
685
|
-
}
|
|
686
624
|
function buildSynthesisPrompt(input) {
|
|
687
625
|
const grouped = /* @__PURE__ */ new Map();
|
|
688
626
|
for (const item of input.evidence) {
|
|
@@ -696,9 +634,10 @@ function buildSynthesisPrompt(input) {
|
|
|
696
634
|
if (items.length === 0) {
|
|
697
635
|
continue;
|
|
698
636
|
}
|
|
699
|
-
const lines = items.map(
|
|
700
|
-
|
|
701
|
-
|
|
637
|
+
const lines = items.map((item, index) => {
|
|
638
|
+
const factSource = typeof item.metadata?.source === "string" && item.metadata.source.length > 0 ? item.metadata.source : item.source ?? "unknown";
|
|
639
|
+
return ` [${index + 1}] (${item.id}, source=${factSource}, score=${item.score.toFixed(4)}) ${item.snippet}`;
|
|
640
|
+
});
|
|
702
641
|
sections.push(`## ${tier}
|
|
703
642
|
${lines.join("\n")}`);
|
|
704
643
|
}
|
|
@@ -780,12 +719,15 @@ function coerceAnswer(text, schema) {
|
|
|
780
719
|
return text;
|
|
781
720
|
}
|
|
782
721
|
async function synthesizeAnswer(input) {
|
|
783
|
-
const { query,
|
|
784
|
-
const evidence =
|
|
785
|
-
id:
|
|
786
|
-
source:
|
|
787
|
-
snippet:
|
|
788
|
-
score:
|
|
722
|
+
const { query, responseSchema, deps, logger } = input;
|
|
723
|
+
const evidence = input.evidence.map((item) => ({
|
|
724
|
+
id: item.id,
|
|
725
|
+
source: mapEvidenceSourceToTier(item.source),
|
|
726
|
+
snippet: item.snippet,
|
|
727
|
+
score: item.score,
|
|
728
|
+
// Forward per-fact provenance (e.g. `metadata.source` from
|
|
729
|
+
// `opencontext add --source`) so the synthesis prompt can cite it.
|
|
730
|
+
metadata: item.metadata
|
|
789
731
|
}));
|
|
790
732
|
const warnings = [];
|
|
791
733
|
const complete = deps.reasoning?.complete;
|
|
@@ -816,21 +758,10 @@ async function synthesizeAnswer(input) {
|
|
|
816
758
|
return { answer: "", warnings };
|
|
817
759
|
}
|
|
818
760
|
}
|
|
819
|
-
function
|
|
820
|
-
|
|
821
|
-
if (
|
|
822
|
-
|
|
823
|
-
}
|
|
824
|
-
if (hit.type === "memory") {
|
|
825
|
-
return "raw";
|
|
826
|
-
}
|
|
827
|
-
if (hit.type === "insight") {
|
|
828
|
-
return "insight";
|
|
829
|
-
}
|
|
830
|
-
if (hit.type === "knowledge") {
|
|
831
|
-
return "knowledge";
|
|
832
|
-
}
|
|
833
|
-
return hit.type;
|
|
761
|
+
function mapEvidenceSourceToTier(source) {
|
|
762
|
+
if (source === "memory") return "raw";
|
|
763
|
+
if (source === "insights") return "insight";
|
|
764
|
+
return source;
|
|
834
765
|
}
|
|
835
766
|
async function resolveSearchScopePeers(input) {
|
|
836
767
|
return resolveScopePeer({
|
|
@@ -854,7 +785,7 @@ function deriveEntityKeywords(query) {
|
|
|
854
785
|
}
|
|
855
786
|
return out.slice(0, 16);
|
|
856
787
|
}
|
|
857
|
-
async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = []) {
|
|
788
|
+
async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = [], runtimeContext) {
|
|
858
789
|
if (typeof deps.entitySearch !== "function") {
|
|
859
790
|
return [];
|
|
860
791
|
}
|
|
@@ -872,7 +803,8 @@ async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = [
|
|
|
872
803
|
keywords,
|
|
873
804
|
limit: Math.ceil(limit / filters.length),
|
|
874
805
|
botId,
|
|
875
|
-
...peerPeers.length > 0 ? { peers: peerPeers } : {}
|
|
806
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
807
|
+
...runtimeContext ?? {}
|
|
876
808
|
})
|
|
877
809
|
)
|
|
878
810
|
)).flat();
|
|
@@ -960,6 +892,16 @@ function filterByDateRange(items, dateFrom, dateTo) {
|
|
|
960
892
|
return true;
|
|
961
893
|
});
|
|
962
894
|
}
|
|
895
|
+
function warnApplicabilityNotEnforced(warnings) {
|
|
896
|
+
if (warnings.some((warning) => warning.code === "memory_applicability_not_enforced")) {
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
warnings.push({
|
|
900
|
+
source: "memory",
|
|
901
|
+
code: "memory_applicability_not_enforced",
|
|
902
|
+
message: "Built-in raw-message retrieval was skipped because it cannot enforce the supplied applicability context. Configure an applicability-aware raw-message provider."
|
|
903
|
+
});
|
|
904
|
+
}
|
|
963
905
|
async function embedQueryVariant(embedQuery, input, query) {
|
|
964
906
|
return embedQuery({
|
|
965
907
|
userId: input.userId,
|
|
@@ -967,10 +909,29 @@ async function embedQueryVariant(embedQuery, input, query) {
|
|
|
967
909
|
authToken: input.authToken
|
|
968
910
|
});
|
|
969
911
|
}
|
|
970
|
-
async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger, peerPeers
|
|
912
|
+
async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger, peerPeers, warnings, runtimeContext) {
|
|
971
913
|
const filters = input.botIds && input.botIds.length > 0 ? input.botIds.map((botId) => ({ botId })) : [{}];
|
|
972
914
|
let semantic = [];
|
|
973
|
-
if (
|
|
915
|
+
if (typeof deps.searchRawMessagesAnn === "function") {
|
|
916
|
+
const searchRawMessagesAnn = deps.searchRawMessagesAnn;
|
|
917
|
+
const factTypes = input.factTypes?.length ? input.factTypes : void 0;
|
|
918
|
+
const includeDeprecated = input.includeDeprecated === true;
|
|
919
|
+
semantic = (await Promise.all(
|
|
920
|
+
filters.map(
|
|
921
|
+
(filter) => searchRawMessagesAnn({
|
|
922
|
+
userId: input.userId,
|
|
923
|
+
queryEmbedding,
|
|
924
|
+
limit,
|
|
925
|
+
threshold,
|
|
926
|
+
botId: "botId" in filter ? filter.botId : void 0,
|
|
927
|
+
includeDeprecated,
|
|
928
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
929
|
+
...factTypes ? { factTypes } : {},
|
|
930
|
+
...runtimeContext ?? {}
|
|
931
|
+
})
|
|
932
|
+
)
|
|
933
|
+
)).flat().filter(isRawMemorySemanticResult).map(toMemoryResult);
|
|
934
|
+
} else if (runtimeContext === void 0 && isRawMessageChromaEnabled()) {
|
|
974
935
|
try {
|
|
975
936
|
semantic = (await Promise.all(
|
|
976
937
|
filters.map((filter) => {
|
|
@@ -995,24 +956,12 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
|
|
|
995
956
|
error
|
|
996
957
|
);
|
|
997
958
|
}
|
|
998
|
-
} else if (typeof deps.searchRawMessagesAnn === "function") {
|
|
999
|
-
const searchRawMessagesAnn = deps.searchRawMessagesAnn;
|
|
1000
|
-
const factTypes = input.factTypes?.length ? input.factTypes : void 0;
|
|
1001
|
-
semantic = (await Promise.all(
|
|
1002
|
-
filters.map(
|
|
1003
|
-
(filter) => searchRawMessagesAnn({
|
|
1004
|
-
userId: input.userId,
|
|
1005
|
-
queryEmbedding,
|
|
1006
|
-
limit,
|
|
1007
|
-
threshold,
|
|
1008
|
-
botId: "botId" in filter ? filter.botId : void 0,
|
|
1009
|
-
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
1010
|
-
...factTypes ? { factTypes } : {}
|
|
1011
|
-
})
|
|
1012
|
-
)
|
|
1013
|
-
)).flat().filter(isRawMemorySemanticResult).map(toMemoryResult);
|
|
1014
959
|
}
|
|
1015
|
-
if (semantic.length === 0) {
|
|
960
|
+
if (semantic.length === 0 && runtimeContext !== void 0) {
|
|
961
|
+
warnApplicabilityNotEnforced(warnings);
|
|
962
|
+
return [];
|
|
963
|
+
}
|
|
964
|
+
if (semantic.length === 0 && typeof deps.searchRawMessagesAnn !== "function") {
|
|
1016
965
|
try {
|
|
1017
966
|
const { getRawMessageManager: getRawMessageManager2 } = await Promise.resolve().then(() => (init_raw_message_store(), raw_message_store_exports));
|
|
1018
967
|
const manager2 = await getRawMessageManager2();
|
|
@@ -1023,6 +972,7 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
|
|
|
1023
972
|
queryEmbedding,
|
|
1024
973
|
limit,
|
|
1025
974
|
threshold,
|
|
975
|
+
includeDeprecated: input.includeDeprecated === true,
|
|
1026
976
|
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
1027
977
|
...factTypes ? { factTypes } : {}
|
|
1028
978
|
});
|
|
@@ -1052,6 +1002,18 @@ function mergeByMaxScore(lists) {
|
|
|
1052
1002
|
}
|
|
1053
1003
|
return Array.from(best.values()).sort((a, b) => b.similarity - a.similarity);
|
|
1054
1004
|
}
|
|
1005
|
+
function dedupeChannelByParent(hits) {
|
|
1006
|
+
const strongest = /* @__PURE__ */ new Map();
|
|
1007
|
+
for (const hit of hits) {
|
|
1008
|
+
const sourceMessageId = hit.metadata?.sourceMessageId;
|
|
1009
|
+
const parentId = typeof sourceMessageId === "string" && sourceMessageId.length > 0 ? sourceMessageId : hit.id;
|
|
1010
|
+
const normalized = hit.type === "memory" && hit.id !== parentId ? { ...hit, id: parentId } : hit;
|
|
1011
|
+
const key = `${normalized.type}:${parentId}`;
|
|
1012
|
+
const current = strongest.get(key);
|
|
1013
|
+
if (!current || normalized.similarity > current.similarity) strongest.set(key, normalized);
|
|
1014
|
+
}
|
|
1015
|
+
return [...strongest.values()].sort((a, b) => b.similarity - a.similarity);
|
|
1016
|
+
}
|
|
1055
1017
|
function toIterativeRecallCandidate(hit) {
|
|
1056
1018
|
return {
|
|
1057
1019
|
id: hit.id,
|
|
@@ -1060,7 +1022,7 @@ function toIterativeRecallCandidate(hit) {
|
|
|
1060
1022
|
metadata: hit.metadata
|
|
1061
1023
|
};
|
|
1062
1024
|
}
|
|
1063
|
-
async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger, peerPeers
|
|
1025
|
+
async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger, peerPeers, warnings, runtimeContext) {
|
|
1064
1026
|
if (keywords.length === 0) {
|
|
1065
1027
|
return [];
|
|
1066
1028
|
}
|
|
@@ -1069,6 +1031,7 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
|
|
|
1069
1031
|
const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
|
|
1070
1032
|
const searchRawMessagesLexical = deps.searchRawMessagesLexical;
|
|
1071
1033
|
const factTypes = input.factTypes?.length ? input.factTypes : void 0;
|
|
1034
|
+
const includeDeprecated = input.includeDeprecated === true;
|
|
1072
1035
|
return (await Promise.all(
|
|
1073
1036
|
filters.map(
|
|
1074
1037
|
(botId) => searchRawMessagesLexical({
|
|
@@ -1076,8 +1039,10 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
|
|
|
1076
1039
|
keywords,
|
|
1077
1040
|
limit: Math.ceil(limit / filters.length),
|
|
1078
1041
|
botId,
|
|
1042
|
+
includeDeprecated,
|
|
1079
1043
|
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
1080
|
-
...factTypes ? { factTypes } : {}
|
|
1044
|
+
...factTypes ? { factTypes } : {},
|
|
1045
|
+
...runtimeContext ?? {}
|
|
1081
1046
|
})
|
|
1082
1047
|
)
|
|
1083
1048
|
)).flat().filter(isRawMemorySemanticResult).map((hit) => ({
|
|
@@ -1088,10 +1053,15 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
|
|
|
1088
1053
|
logger.warn?.("[memory-store] lexical memory search failed:", error);
|
|
1089
1054
|
}
|
|
1090
1055
|
}
|
|
1056
|
+
if (runtimeContext !== void 0) {
|
|
1057
|
+
warnApplicabilityNotEnforced(warnings);
|
|
1058
|
+
return [];
|
|
1059
|
+
}
|
|
1091
1060
|
try {
|
|
1092
1061
|
const { lexicalSearchRawMessages: lexicalSearchRawMessages2 } = await Promise.resolve().then(() => (init_sqlite_raw_message_store(), sqlite_raw_message_store_exports));
|
|
1093
1062
|
const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
|
|
1094
1063
|
const factTypes = input.factTypes?.length ? input.factTypes : void 0;
|
|
1064
|
+
const includeDeprecated = input.includeDeprecated === true;
|
|
1095
1065
|
return (await Promise.all(
|
|
1096
1066
|
filters.map(
|
|
1097
1067
|
(botId) => lexicalSearchRawMessages2({
|
|
@@ -1099,6 +1069,7 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
|
|
|
1099
1069
|
keywords,
|
|
1100
1070
|
limit: Math.ceil(limit / filters.length),
|
|
1101
1071
|
botId,
|
|
1072
|
+
includeDeprecated,
|
|
1102
1073
|
...factTypes ? { factTypes } : {}
|
|
1103
1074
|
})
|
|
1104
1075
|
)
|
|
@@ -1129,7 +1100,9 @@ function searchInputToUnified(input) {
|
|
|
1129
1100
|
mergeStrategy: input.mergeStrategy,
|
|
1130
1101
|
peerFilter: input.peerFilter,
|
|
1131
1102
|
reasoningStrategy: input.reasoningStrategy,
|
|
1132
|
-
factTypes: input.factTypes
|
|
1103
|
+
factTypes: input.factTypes,
|
|
1104
|
+
includeRetrievalDiagnostics: input.includeRetrievalDiagnostics,
|
|
1105
|
+
includeDeprecated: input.includeDeprecated
|
|
1133
1106
|
};
|
|
1134
1107
|
}
|
|
1135
1108
|
function mapHitToEvidenceSource(hit) {
|
|
@@ -1157,7 +1130,7 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1157
1130
|
}
|
|
1158
1131
|
return normalizeUnifiedMemoryReasoningStrategy(deps.reasoning?.defaultStrategy, "none");
|
|
1159
1132
|
}
|
|
1160
|
-
async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = []) {
|
|
1133
|
+
async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = [], runtimeContext) {
|
|
1161
1134
|
if ((reasoningStrategy === "iterative" || reasoningStrategy === "union") && !deps.reasoning?.iterativePlanner) {
|
|
1162
1135
|
warnings.push({
|
|
1163
1136
|
source: "memory",
|
|
@@ -1190,7 +1163,9 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1190
1163
|
keywords2,
|
|
1191
1164
|
limit,
|
|
1192
1165
|
logger,
|
|
1193
|
-
peerPeers
|
|
1166
|
+
peerPeers,
|
|
1167
|
+
warnings,
|
|
1168
|
+
runtimeContext
|
|
1194
1169
|
);
|
|
1195
1170
|
let semanticHits = [];
|
|
1196
1171
|
if (typeof deps.embedQuery === "function") {
|
|
@@ -1204,7 +1179,9 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1204
1179
|
limit,
|
|
1205
1180
|
threshold,
|
|
1206
1181
|
logger,
|
|
1207
|
-
peerPeers
|
|
1182
|
+
peerPeers,
|
|
1183
|
+
warnings,
|
|
1184
|
+
runtimeContext
|
|
1208
1185
|
);
|
|
1209
1186
|
} catch (error) {
|
|
1210
1187
|
logger.warn?.(
|
|
@@ -1245,6 +1222,11 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1245
1222
|
unionEvidence = evidence;
|
|
1246
1223
|
}
|
|
1247
1224
|
if (typeof deps.embedQuery !== "function") {
|
|
1225
|
+
warnings.push({
|
|
1226
|
+
source: "memory",
|
|
1227
|
+
code: "semantic_unavailable",
|
|
1228
|
+
message: "Semantic search is unavailable because no embedding provider is configured."
|
|
1229
|
+
});
|
|
1248
1230
|
warnings.push({
|
|
1249
1231
|
source: "memory",
|
|
1250
1232
|
code: "memory_lexical_search_fallback",
|
|
@@ -1252,7 +1234,16 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1252
1234
|
});
|
|
1253
1235
|
const keywords2 = deriveLexicalKeywords(input.query);
|
|
1254
1236
|
const lexical2 = filterByDateRange(
|
|
1255
|
-
await runLexicalSearchForKeywords(
|
|
1237
|
+
await runLexicalSearchForKeywords(
|
|
1238
|
+
deps,
|
|
1239
|
+
input,
|
|
1240
|
+
keywords2,
|
|
1241
|
+
limit,
|
|
1242
|
+
logger,
|
|
1243
|
+
peerPeers,
|
|
1244
|
+
warnings,
|
|
1245
|
+
runtimeContext
|
|
1246
|
+
),
|
|
1256
1247
|
input.dateFrom,
|
|
1257
1248
|
input.dateTo
|
|
1258
1249
|
);
|
|
@@ -1260,6 +1251,26 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1260
1251
|
}
|
|
1261
1252
|
let semantic = [];
|
|
1262
1253
|
const embedQuery = deps.embedQuery;
|
|
1254
|
+
const activeMergeStrategy = normalizeUnifiedMemoryMergeStrategy(
|
|
1255
|
+
input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
|
|
1256
|
+
);
|
|
1257
|
+
if (reasoningStrategy === "none" && activeMergeStrategy === "rrf" && input.threshold === void 0 && typeof deps.searchRawMessagesHybrid === "function") {
|
|
1258
|
+
const queryEmbedding = await embedQueryVariant(embedQuery, input, input.query);
|
|
1259
|
+
const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
|
|
1260
|
+
const hybrid = (await Promise.all(
|
|
1261
|
+
filters.map(
|
|
1262
|
+
(botId) => deps.searchRawMessagesHybrid?.({
|
|
1263
|
+
userId: input.userId,
|
|
1264
|
+
query: input.query,
|
|
1265
|
+
queryEmbedding,
|
|
1266
|
+
limit: Math.ceil(limit / filters.length),
|
|
1267
|
+
botId,
|
|
1268
|
+
...runtimeContext ?? {}
|
|
1269
|
+
})
|
|
1270
|
+
)
|
|
1271
|
+
)).flatMap((items) => items ?? []).filter(isRawMemorySemanticResult).map(toMemoryResult);
|
|
1272
|
+
return { semantic: [], lexical: [], hybrid: filterByDateRange(hybrid, input.dateFrom, input.dateTo) };
|
|
1273
|
+
}
|
|
1263
1274
|
if (reasoningStrategy === "rewrite" && deps.reasoning?.queryRewriter) {
|
|
1264
1275
|
try {
|
|
1265
1276
|
const variants = await deps.reasoning.queryRewriter.rewrite({
|
|
@@ -1278,7 +1289,17 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1278
1289
|
);
|
|
1279
1290
|
const lists = await Promise.all(
|
|
1280
1291
|
embeddings.map(
|
|
1281
|
-
(embedding) => runSemanticSearchForEmbedding(
|
|
1292
|
+
(embedding) => runSemanticSearchForEmbedding(
|
|
1293
|
+
deps,
|
|
1294
|
+
input,
|
|
1295
|
+
embedding,
|
|
1296
|
+
limit,
|
|
1297
|
+
threshold,
|
|
1298
|
+
logger,
|
|
1299
|
+
peerPeers,
|
|
1300
|
+
warnings,
|
|
1301
|
+
runtimeContext
|
|
1302
|
+
)
|
|
1282
1303
|
)
|
|
1283
1304
|
);
|
|
1284
1305
|
semantic = mergeByMaxScore(lists);
|
|
@@ -1301,7 +1322,9 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1301
1322
|
limit,
|
|
1302
1323
|
threshold,
|
|
1303
1324
|
logger,
|
|
1304
|
-
peerPeers
|
|
1325
|
+
peerPeers,
|
|
1326
|
+
warnings,
|
|
1327
|
+
runtimeContext
|
|
1305
1328
|
);
|
|
1306
1329
|
}
|
|
1307
1330
|
let lexical = [];
|
|
@@ -1320,7 +1343,8 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1320
1343
|
limit,
|
|
1321
1344
|
botId,
|
|
1322
1345
|
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
1323
|
-
...factTypes ? { factTypes } : {}
|
|
1346
|
+
...factTypes ? { factTypes } : {},
|
|
1347
|
+
...runtimeContext ?? {}
|
|
1324
1348
|
})
|
|
1325
1349
|
)
|
|
1326
1350
|
)).flat().filter(isRawMemorySemanticResult).map((hit) => ({
|
|
@@ -1346,7 +1370,7 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1346
1370
|
let entity;
|
|
1347
1371
|
if (typeof deps.entitySearch === "function") {
|
|
1348
1372
|
entity = filterByDateRange(
|
|
1349
|
-
await runEntitySearchForQuery(deps, input, limit, logger, peerPeers),
|
|
1373
|
+
await runEntitySearchForQuery(deps, input, limit, logger, peerPeers, runtimeContext),
|
|
1350
1374
|
input.dateFrom,
|
|
1351
1375
|
input.dateTo
|
|
1352
1376
|
);
|
|
@@ -1408,14 +1432,16 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1408
1432
|
}
|
|
1409
1433
|
return out;
|
|
1410
1434
|
}
|
|
1411
|
-
async function
|
|
1435
|
+
async function searchUnifiedMemoryWithRuntime(input, runtimeContext) {
|
|
1412
1436
|
const query = input.query.trim();
|
|
1413
1437
|
const sources = normalizeUnifiedMemorySearchSources(input.sources);
|
|
1414
1438
|
const limit = clampUnifiedMemorySearchLimit(input.limit);
|
|
1415
|
-
const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
|
|
1416
1439
|
const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
|
|
1417
1440
|
input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
|
|
1418
1441
|
);
|
|
1442
|
+
const candidateLimit = Math.min(50, Math.max(limit, limit * 4));
|
|
1443
|
+
const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
|
|
1444
|
+
const memoryThreshold = input.threshold === void 0 && mergeStrategy === "rrf" ? Number.NEGATIVE_INFINITY : threshold;
|
|
1419
1445
|
const reasoningStrategy = resolveReasoningStrategy(input);
|
|
1420
1446
|
const warnings = [];
|
|
1421
1447
|
const peerScope = input.peerFilter && input.peerFilter.length > 0 ? await resolveScopePeer({
|
|
@@ -1435,23 +1461,46 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1435
1461
|
};
|
|
1436
1462
|
}
|
|
1437
1463
|
let memorySubs;
|
|
1464
|
+
let retrievalStatus;
|
|
1438
1465
|
const reasoningInfo = { strategy: reasoningStrategy };
|
|
1439
1466
|
if (input.dateFrom || input.dateTo) {
|
|
1440
1467
|
reasoningInfo.dateRange = { from: input.dateFrom, to: input.dateTo };
|
|
1441
1468
|
}
|
|
1442
1469
|
if (sources.includes("memory")) {
|
|
1443
|
-
const hasRawProviders = typeof deps.searchRawMessagesAnn === "function" || typeof deps.searchRawMessagesLexical === "function";
|
|
1470
|
+
const hasRawProviders = typeof deps.searchRawMessagesAnn === "function" || typeof deps.searchRawMessagesHybrid === "function" || typeof deps.searchRawMessagesLexical === "function";
|
|
1444
1471
|
if (isRawMessageStorageAvailable() || hasRawProviders) {
|
|
1445
1472
|
try {
|
|
1446
1473
|
memorySubs = await runMemorySource(
|
|
1447
1474
|
input,
|
|
1448
|
-
|
|
1449
|
-
|
|
1475
|
+
candidateLimit,
|
|
1476
|
+
memoryThreshold,
|
|
1450
1477
|
warnings,
|
|
1451
1478
|
reasoningStrategy,
|
|
1452
1479
|
reasoningInfo,
|
|
1453
|
-
peerPeers
|
|
1480
|
+
peerPeers,
|
|
1481
|
+
runtimeContext
|
|
1454
1482
|
);
|
|
1483
|
+
memorySubs = {
|
|
1484
|
+
semantic: dedupeChannelByParent(memorySubs.semantic),
|
|
1485
|
+
lexical: dedupeChannelByParent(memorySubs.lexical),
|
|
1486
|
+
...memorySubs.hybrid ? { hybrid: dedupeChannelByParent(memorySubs.hybrid) } : {},
|
|
1487
|
+
...memorySubs.entity ? { entity: dedupeChannelByParent(memorySubs.entity) } : {}
|
|
1488
|
+
};
|
|
1489
|
+
if (deps.getRawMessageRetrievalStatus) {
|
|
1490
|
+
retrievalStatus = await deps.getRawMessageRetrievalStatus();
|
|
1491
|
+
}
|
|
1492
|
+
if (memorySubs.semantic.length === 0 && (memorySubs.hybrid?.length ?? 0) === 0 && typeof deps.embedQuery === "function") {
|
|
1493
|
+
let code = "semantic_candidates_empty";
|
|
1494
|
+
let message = "Semantic search was available but returned no candidates.";
|
|
1495
|
+
if (retrievalStatus?.embeddedChildCount === 0) {
|
|
1496
|
+
code = "semantic_unavailable";
|
|
1497
|
+
message = "No embedded RawMessage children are available for semantic search.";
|
|
1498
|
+
} else if (retrievalStatus?.embeddingDimensions && retrievalStatus.indexedDimensions.length > 0 && !retrievalStatus.indexedDimensions.includes(retrievalStatus.embeddingDimensions)) {
|
|
1499
|
+
code = "semantic_dimension_mismatch";
|
|
1500
|
+
message = `Query embedding dimension ${retrievalStatus.embeddingDimensions} does not match indexed child dimensions ${retrievalStatus.indexedDimensions.join(", ")}.`;
|
|
1501
|
+
}
|
|
1502
|
+
warnings.push({ source: "memory", code, message });
|
|
1503
|
+
}
|
|
1455
1504
|
} catch (error) {
|
|
1456
1505
|
logger.warn?.("[memory-store] memory source failed:", error);
|
|
1457
1506
|
warnings.push({
|
|
@@ -1474,12 +1523,13 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1474
1523
|
const hits = await deps.searchInsights({
|
|
1475
1524
|
userId: input.userId,
|
|
1476
1525
|
query: input.query,
|
|
1477
|
-
limit,
|
|
1526
|
+
limit: candidateLimit,
|
|
1478
1527
|
threshold,
|
|
1479
1528
|
botIds: input.botIds,
|
|
1480
1529
|
includeArchived: input.includeArchivedInsights,
|
|
1481
1530
|
authToken: input.authToken,
|
|
1482
|
-
...peerPeers.length > 0 ? { peers: peerPeers } : {}
|
|
1531
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
1532
|
+
...runtimeContext ?? {}
|
|
1483
1533
|
});
|
|
1484
1534
|
for (const hit of hits) {
|
|
1485
1535
|
insightHits.push({
|
|
@@ -1506,12 +1556,13 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1506
1556
|
userId: input.userId,
|
|
1507
1557
|
query: input.query,
|
|
1508
1558
|
options: {
|
|
1509
|
-
limit,
|
|
1559
|
+
limit: candidateLimit,
|
|
1510
1560
|
threshold,
|
|
1511
1561
|
documentIds: input.documentIds
|
|
1512
1562
|
},
|
|
1513
1563
|
authToken: input.authToken,
|
|
1514
|
-
...peerPeers.length > 0 ? { peers: peerPeers } : {}
|
|
1564
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
1565
|
+
...runtimeContext ?? {}
|
|
1515
1566
|
});
|
|
1516
1567
|
for (const hit of hits) {
|
|
1517
1568
|
knowledgeHits.push(toKnowledgeResult(hit));
|
|
@@ -1529,10 +1580,14 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1529
1580
|
memorySubs,
|
|
1530
1581
|
insightHits,
|
|
1531
1582
|
knowledgeHits,
|
|
1532
|
-
limit,
|
|
1583
|
+
limit: candidateLimit,
|
|
1533
1584
|
strategy: mergeStrategy
|
|
1534
1585
|
});
|
|
1535
|
-
const
|
|
1586
|
+
const rerankerStartedAt = deps.reranker ? Date.now() : void 0;
|
|
1587
|
+
const reranked = await applyReranker(deps.reranker, input.query, merged);
|
|
1588
|
+
const rerankerLatencyMs = rerankerStartedAt === void 0 ? 0 : Date.now() - rerankerStartedAt;
|
|
1589
|
+
const rerankerOrderChanged = Boolean(deps.reranker) && merged.some((hit, index) => hit.id !== reranked[index]?.id);
|
|
1590
|
+
const ranked = reranked.slice(0, limit);
|
|
1536
1591
|
const output = {
|
|
1537
1592
|
query: input.query,
|
|
1538
1593
|
sources,
|
|
@@ -1540,28 +1595,51 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1540
1595
|
count: ranked.length,
|
|
1541
1596
|
warnings
|
|
1542
1597
|
};
|
|
1598
|
+
if (input.includeRetrievalDiagnostics) {
|
|
1599
|
+
output.retrievalDiagnostics = {
|
|
1600
|
+
mergeStrategy,
|
|
1601
|
+
candidateLimit,
|
|
1602
|
+
backend: retrievalStatus?.backend,
|
|
1603
|
+
semanticDegradedReason: retrievalStatus?.semanticDegradedReason ?? warnings.find((warning) => warning.code.startsWith("semantic_"))?.code,
|
|
1604
|
+
candidateCounts: {
|
|
1605
|
+
semantic: memorySubs?.semantic.length ?? 0,
|
|
1606
|
+
lexical: memorySubs?.lexical.length ?? 0,
|
|
1607
|
+
hybrid: memorySubs?.hybrid?.length ?? 0,
|
|
1608
|
+
entity: memorySubs?.entity?.length ?? 0,
|
|
1609
|
+
fused: merged.length,
|
|
1610
|
+
final: ranked.length
|
|
1611
|
+
},
|
|
1612
|
+
channels: {
|
|
1613
|
+
semantic: memorySubs?.semantic ?? [],
|
|
1614
|
+
lexical: memorySubs?.lexical ?? [],
|
|
1615
|
+
...memorySubs?.hybrid ? { hybrid: memorySubs.hybrid } : {},
|
|
1616
|
+
...memorySubs?.entity ? { entity: memorySubs.entity } : {}
|
|
1617
|
+
},
|
|
1618
|
+
fusedBeforeRerank: merged,
|
|
1619
|
+
reranker: {
|
|
1620
|
+
enabled: Boolean(deps.reranker),
|
|
1621
|
+
provider: deps.rerankerInfo?.provider,
|
|
1622
|
+
model: deps.rerankerInfo?.model,
|
|
1623
|
+
inputCount: merged.length,
|
|
1624
|
+
outputCount: reranked.length,
|
|
1625
|
+
latencyMs: rerankerLatencyMs,
|
|
1626
|
+
orderChanged: rerankerOrderChanged
|
|
1627
|
+
},
|
|
1628
|
+
final: ranked
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1543
1631
|
if (reasoningInfo && (reasoningInfo.strategy !== "none" || reasoningInfo.dateRange !== void 0)) {
|
|
1544
1632
|
output.reasoning = reasoningInfo;
|
|
1545
1633
|
}
|
|
1546
1634
|
return output;
|
|
1547
1635
|
}
|
|
1636
|
+
async function searchUnifiedMemory(input) {
|
|
1637
|
+
return searchUnifiedMemoryWithRuntime(input);
|
|
1638
|
+
}
|
|
1548
1639
|
async function searchRawMemorySemantically(input) {
|
|
1549
|
-
|
|
1550
|
-
const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
|
|
1551
|
-
const reasoningStrategy = resolveReasoningStrategy(input);
|
|
1552
|
-
const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
|
|
1553
|
-
input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
|
|
1554
|
-
);
|
|
1555
|
-
const sub = await runMemorySource(input, limit, threshold, [], reasoningStrategy);
|
|
1556
|
-
return mergeAcrossSources({
|
|
1557
|
-
memorySubs: sub,
|
|
1558
|
-
insightHits: [],
|
|
1559
|
-
knowledgeHits: [],
|
|
1560
|
-
limit,
|
|
1561
|
-
strategy: mergeStrategy
|
|
1562
|
-
});
|
|
1640
|
+
return (await searchUnifiedMemory({ ...input, sources: ["memory"] })).results;
|
|
1563
1641
|
}
|
|
1564
|
-
async function
|
|
1642
|
+
async function searchWithResolvedRuntime(input, runtimeContext) {
|
|
1565
1643
|
const query = input.query.trim();
|
|
1566
1644
|
const wantsSynthesis = typeof input.synthesize === "boolean" ? input.synthesize : typeof input.synthesize === "object" && input.synthesize !== null;
|
|
1567
1645
|
const synthesisSchema = typeof input.synthesize === "object" && input.synthesize !== null ? input.synthesize.responseSchema ?? input.responseSchema : input.responseSchema;
|
|
@@ -1593,19 +1671,24 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1593
1671
|
let unifiedHits = [];
|
|
1594
1672
|
let sources = input.sources ? [...input.sources] : [];
|
|
1595
1673
|
let unifiedReasoning;
|
|
1674
|
+
let retrievalDiagnostics;
|
|
1596
1675
|
if (wantsRawTier || wantsInsightTier || wantsKnowledgeTier) {
|
|
1597
1676
|
const inferredSources = sources.length > 0 ? sources : [
|
|
1598
1677
|
...wantsRawTier ? ["memory"] : [],
|
|
1599
1678
|
...wantsInsightTier ? ["insights"] : [],
|
|
1600
1679
|
...wantsKnowledgeTier ? ["knowledge"] : []
|
|
1601
1680
|
];
|
|
1602
|
-
const unified = await
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1681
|
+
const unified = await searchUnifiedMemoryWithRuntime(
|
|
1682
|
+
{
|
|
1683
|
+
...searchInputToUnified(input),
|
|
1684
|
+
sources: inferredSources.length > 0 ? inferredSources : void 0
|
|
1685
|
+
},
|
|
1686
|
+
runtimeContext
|
|
1687
|
+
);
|
|
1606
1688
|
unifiedHits = unified.results;
|
|
1607
1689
|
sources = unified.sources;
|
|
1608
1690
|
unifiedReasoning = unified.reasoning;
|
|
1691
|
+
retrievalDiagnostics = unified.retrievalDiagnostics;
|
|
1609
1692
|
warnings.push(...unified.warnings);
|
|
1610
1693
|
}
|
|
1611
1694
|
let summaryHits = [];
|
|
@@ -1616,7 +1699,8 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1616
1699
|
limit,
|
|
1617
1700
|
threshold,
|
|
1618
1701
|
logger,
|
|
1619
|
-
peerScope.peers
|
|
1702
|
+
peerScope.peers,
|
|
1703
|
+
runtimeContext
|
|
1620
1704
|
);
|
|
1621
1705
|
warnings.push(...summaryBucket.warnings);
|
|
1622
1706
|
summaryHits = summaryBucket.hits;
|
|
@@ -1641,7 +1725,8 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1641
1725
|
source: mapHitToEvidenceSource(hit),
|
|
1642
1726
|
snippet: hit.content,
|
|
1643
1727
|
score: hit.similarity,
|
|
1644
|
-
timestamp: getCandidateTimestamp(hit.metadata)
|
|
1728
|
+
timestamp: getCandidateTimestamp(hit.metadata),
|
|
1729
|
+
metadata: hit.metadata
|
|
1645
1730
|
}));
|
|
1646
1731
|
const base = {
|
|
1647
1732
|
query,
|
|
@@ -1654,12 +1739,15 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1654
1739
|
if (unifiedReasoning) {
|
|
1655
1740
|
base.reasoning = unifiedReasoning;
|
|
1656
1741
|
}
|
|
1742
|
+
if (retrievalDiagnostics) {
|
|
1743
|
+
base.retrievalDiagnostics = retrievalDiagnostics;
|
|
1744
|
+
}
|
|
1657
1745
|
if (!wantsSynthesis) {
|
|
1658
1746
|
return base;
|
|
1659
1747
|
}
|
|
1660
1748
|
const { answer, warnings: synthWarnings } = await synthesizeAnswer({
|
|
1661
1749
|
query,
|
|
1662
|
-
|
|
1750
|
+
evidence,
|
|
1663
1751
|
responseSchema: synthesisSchema,
|
|
1664
1752
|
deps,
|
|
1665
1753
|
logger
|
|
@@ -1668,6 +1756,10 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1668
1756
|
base.warnings = [...base.warnings, ...synthWarnings];
|
|
1669
1757
|
return base;
|
|
1670
1758
|
}
|
|
1759
|
+
async function search(input, runtimeContext) {
|
|
1760
|
+
const resolvedRuntime = resolveSearchRuntimeContext(input, runtimeContext);
|
|
1761
|
+
return searchWithResolvedRuntime(input, resolvedRuntime);
|
|
1762
|
+
}
|
|
1671
1763
|
return {
|
|
1672
1764
|
search,
|
|
1673
1765
|
searchUnifiedMemory,
|
|
@@ -1683,6 +1775,7 @@ function mergeAcrossSources(input) {
|
|
|
1683
1775
|
for (const hit of [
|
|
1684
1776
|
...memorySubs.semantic,
|
|
1685
1777
|
...memorySubs.lexical,
|
|
1778
|
+
...memorySubs.hybrid ?? [],
|
|
1686
1779
|
...input.insightHits,
|
|
1687
1780
|
...input.knowledgeHits
|
|
1688
1781
|
]) {
|
|
@@ -1698,6 +1791,7 @@ function mergeAcrossSources(input) {
|
|
|
1698
1791
|
const all = [
|
|
1699
1792
|
...memorySubs.semantic,
|
|
1700
1793
|
...memorySubs.lexical,
|
|
1794
|
+
...memorySubs.hybrid ?? [],
|
|
1701
1795
|
...memorySubs.entity ?? [],
|
|
1702
1796
|
...input.insightHits,
|
|
1703
1797
|
...input.knowledgeHits
|
|
@@ -1709,6 +1803,9 @@ function buildChannelLists(memorySubs, insightHits, knowledgeHits) {
|
|
|
1709
1803
|
const lists = [];
|
|
1710
1804
|
if (memorySubs.semantic.length > 0) lists.push({ name: "memory-semantic", hits: memorySubs.semantic });
|
|
1711
1805
|
if (memorySubs.lexical.length > 0) lists.push({ name: "memory-bm25", hits: memorySubs.lexical });
|
|
1806
|
+
if (memorySubs.hybrid && memorySubs.hybrid.length > 0) {
|
|
1807
|
+
lists.push({ name: "memory-hybrid", hits: memorySubs.hybrid });
|
|
1808
|
+
}
|
|
1712
1809
|
if (memorySubs.entity && memorySubs.entity.length > 0) {
|
|
1713
1810
|
lists.push({ name: "memory-entity", hits: memorySubs.entity });
|
|
1714
1811
|
}
|
|
@@ -2173,8 +2270,250 @@ function createVsaRecall(storage) {
|
|
|
2173
2270
|
};
|
|
2174
2271
|
}
|
|
2175
2272
|
|
|
2273
|
+
// ../shared/src/tokens.ts
|
|
2274
|
+
var estimateTokens = (text) => {
|
|
2275
|
+
const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length;
|
|
2276
|
+
const otherChars = text.length - chineseChars;
|
|
2277
|
+
const wordCount = Math.ceil(otherChars / 5);
|
|
2278
|
+
return chineseChars + wordCount;
|
|
2279
|
+
};
|
|
2280
|
+
|
|
2281
|
+
// ../shared/src/text-chunking.ts
|
|
2282
|
+
var RAW_MESSAGE_CHUNK_MAX_TOKENS = 400;
|
|
2283
|
+
var RAW_MESSAGE_CHUNK_OVERLAP_TOKENS = 80;
|
|
2284
|
+
var PARAGRAPH_BOUNDARIES = ["\n\n", "\r\n\r\n"];
|
|
2285
|
+
var SENTENCE_BOUNDARY = /[.!?。!?;;]\s*$/u;
|
|
2286
|
+
function largestEndWithinTokenBudget(text, start, maxTokens) {
|
|
2287
|
+
let low = start + 1;
|
|
2288
|
+
let high = text.length;
|
|
2289
|
+
let best = low;
|
|
2290
|
+
while (low <= high) {
|
|
2291
|
+
const middle = Math.floor((low + high) / 2);
|
|
2292
|
+
if (estimateTokens(text.slice(start, middle)) <= maxTokens) {
|
|
2293
|
+
best = middle;
|
|
2294
|
+
low = middle + 1;
|
|
2295
|
+
} else {
|
|
2296
|
+
high = middle - 1;
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
return best;
|
|
2300
|
+
}
|
|
2301
|
+
function preferNaturalEnd(text, start, hardEnd) {
|
|
2302
|
+
if (hardEnd >= text.length) return text.length;
|
|
2303
|
+
const searchStart = start + Math.floor((hardEnd - start) * 0.6);
|
|
2304
|
+
const candidate = text.slice(searchStart, hardEnd);
|
|
2305
|
+
for (const boundary of PARAGRAPH_BOUNDARIES) {
|
|
2306
|
+
const index = candidate.lastIndexOf(boundary);
|
|
2307
|
+
if (index >= 0) return searchStart + index + boundary.length;
|
|
2308
|
+
}
|
|
2309
|
+
for (let offset = candidate.length; offset > 0; offset -= 1) {
|
|
2310
|
+
const prefix = candidate.slice(0, offset);
|
|
2311
|
+
if (SENTENCE_BOUNDARY.test(prefix)) return searchStart + offset;
|
|
2312
|
+
}
|
|
2313
|
+
for (let offset = candidate.length - 1; offset >= 0; offset -= 1) {
|
|
2314
|
+
if (/\s/u.test(candidate[offset] ?? "")) return searchStart + offset + 1;
|
|
2315
|
+
}
|
|
2316
|
+
return hardEnd;
|
|
2317
|
+
}
|
|
2318
|
+
function overlapStart(text, chunkStart, chunkEnd, overlapTokens) {
|
|
2319
|
+
if (overlapTokens <= 0) return chunkEnd;
|
|
2320
|
+
let low = chunkStart + 1;
|
|
2321
|
+
let high = chunkEnd;
|
|
2322
|
+
let best = chunkEnd;
|
|
2323
|
+
while (low <= high) {
|
|
2324
|
+
const middle = Math.floor((low + high) / 2);
|
|
2325
|
+
if (estimateTokens(text.slice(middle, chunkEnd)) <= overlapTokens) {
|
|
2326
|
+
best = middle;
|
|
2327
|
+
high = middle - 1;
|
|
2328
|
+
} else {
|
|
2329
|
+
low = middle + 1;
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
return Math.min(chunkEnd, Math.max(chunkStart + 1, best));
|
|
2333
|
+
}
|
|
2334
|
+
function chunkTextByEstimatedTokens(text, options = {}) {
|
|
2335
|
+
if (text.length === 0) return [];
|
|
2336
|
+
const maxTokens = Math.max(1, Math.floor(options.maxTokens ?? RAW_MESSAGE_CHUNK_MAX_TOKENS));
|
|
2337
|
+
const overlapTokens = Math.max(0, Math.floor(options.overlapTokens ?? RAW_MESSAGE_CHUNK_OVERLAP_TOKENS));
|
|
2338
|
+
if (overlapTokens >= maxTokens) {
|
|
2339
|
+
throw new Error("overlapTokens must be smaller than maxTokens");
|
|
2340
|
+
}
|
|
2341
|
+
const chunks = [];
|
|
2342
|
+
let start = 0;
|
|
2343
|
+
while (start < text.length) {
|
|
2344
|
+
const hardEnd = largestEndWithinTokenBudget(text, start, maxTokens);
|
|
2345
|
+
const end = preferNaturalEnd(text, start, hardEnd);
|
|
2346
|
+
chunks.push({
|
|
2347
|
+
chunkIndex: chunks.length,
|
|
2348
|
+
startPosition: start,
|
|
2349
|
+
endPosition: end,
|
|
2350
|
+
content: text.slice(start, end)
|
|
2351
|
+
});
|
|
2352
|
+
if (end >= text.length) break;
|
|
2353
|
+
start = overlapStart(text, start, end, overlapTokens);
|
|
2354
|
+
}
|
|
2355
|
+
return chunks;
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
// src/embed-on-insert.ts
|
|
2359
|
+
var AUTO_APPLIED_WARNING = {
|
|
2360
|
+
code: "embed_on_insert_auto_applied",
|
|
2361
|
+
message: "Caller did not explicitly enable embedOnInsert, but an embedding provider is configured. OpenContext generated the missing search embeddings."
|
|
2362
|
+
};
|
|
2363
|
+
var SEMANTIC_UNAVAILABLE_WARNING = {
|
|
2364
|
+
code: "semantic_index_unavailable",
|
|
2365
|
+
message: "No embedding provider is configured. RawMessage children were indexed for lexical search only; semantic search is unavailable until they are explicitly reindexed."
|
|
2366
|
+
};
|
|
2367
|
+
function sha256(value) {
|
|
2368
|
+
return createHash("sha256").update(value).digest("hex");
|
|
2369
|
+
}
|
|
2370
|
+
function isUsableEmbedding(embedding) {
|
|
2371
|
+
return Boolean(
|
|
2372
|
+
embedding?.length && embedding.every(Number.isFinite) && embedding.some((value) => value !== 0)
|
|
2373
|
+
);
|
|
2374
|
+
}
|
|
2375
|
+
async function embedText(text, userId, deps) {
|
|
2376
|
+
if (typeof deps.embedQuery !== "function" || text.length === 0) return void 0;
|
|
2377
|
+
const embedding = await deps.embedQuery({ userId, query: text });
|
|
2378
|
+
return isUsableEmbedding(embedding) ? embedding : void 0;
|
|
2379
|
+
}
|
|
2380
|
+
async function embedMissingMessages(messages, deps) {
|
|
2381
|
+
if (typeof deps.embedQuery !== "function") return messages;
|
|
2382
|
+
const out = [];
|
|
2383
|
+
for (const message of messages) {
|
|
2384
|
+
if (isUsableEmbedding(message.embedding) || !message.content) {
|
|
2385
|
+
out.push(message);
|
|
2386
|
+
continue;
|
|
2387
|
+
}
|
|
2388
|
+
const embedding = await embedText(message.content, message.userId, deps);
|
|
2389
|
+
out.push(
|
|
2390
|
+
embedding ? {
|
|
2391
|
+
...message,
|
|
2392
|
+
embedding,
|
|
2393
|
+
embeddingModel: message.embeddingModel ?? deps.embeddingInfo?.model ?? "server",
|
|
2394
|
+
embeddingContentHash: sha256(message.content),
|
|
2395
|
+
embeddingDimensions: embedding.length,
|
|
2396
|
+
embeddingUpdatedAt: Date.now()
|
|
2397
|
+
} : message
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
2400
|
+
return out;
|
|
2401
|
+
}
|
|
2402
|
+
async function prepareRawMessageIngest(incoming, embedOnInsert, unified) {
|
|
2403
|
+
const deps = unified ?? {};
|
|
2404
|
+
const embedderAvailable = typeof deps.embedQuery === "function" || typeof deps.embedDocuments === "function";
|
|
2405
|
+
const shouldEmbed = embedOnInsert === true || embedderAvailable;
|
|
2406
|
+
const messages = [];
|
|
2407
|
+
const chunks = [];
|
|
2408
|
+
let missingSemanticEmbedding = false;
|
|
2409
|
+
let generatedEmbedding = false;
|
|
2410
|
+
const plans = incoming.map((message) => ({
|
|
2411
|
+
message,
|
|
2412
|
+
pieces: chunkTextByEstimatedTokens(message.content)
|
|
2413
|
+
}));
|
|
2414
|
+
const generatedEmbeddings = /* @__PURE__ */ new Map();
|
|
2415
|
+
const pendingByUser = /* @__PURE__ */ new Map();
|
|
2416
|
+
for (const [messageIndex, plan] of plans.entries()) {
|
|
2417
|
+
for (const piece of plan.pieces) {
|
|
2418
|
+
const canReuseParentEmbedding = plan.pieces.length === 1 && isUsableEmbedding(plan.message.embedding);
|
|
2419
|
+
if (canReuseParentEmbedding || !shouldEmbed || piece.content.length === 0) continue;
|
|
2420
|
+
const pending = pendingByUser.get(plan.message.userId) ?? [];
|
|
2421
|
+
pending.push({ key: `${messageIndex}:${piece.chunkIndex}`, text: piece.content });
|
|
2422
|
+
pendingByUser.set(plan.message.userId, pending);
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
for (const [userId, pending] of pendingByUser) {
|
|
2426
|
+
let embeddings;
|
|
2427
|
+
if (typeof deps.embedDocuments === "function") {
|
|
2428
|
+
const batch = await deps.embedDocuments({
|
|
2429
|
+
userId,
|
|
2430
|
+
texts: pending.map((item) => item.text)
|
|
2431
|
+
});
|
|
2432
|
+
if (batch.length !== pending.length) {
|
|
2433
|
+
throw new Error(
|
|
2434
|
+
`Document embedding count mismatch: expected ${pending.length}, received ${batch.length}`
|
|
2435
|
+
);
|
|
2436
|
+
}
|
|
2437
|
+
embeddings = batch.map((embedding) => isUsableEmbedding(embedding) ? embedding : void 0);
|
|
2438
|
+
} else {
|
|
2439
|
+
embeddings = [];
|
|
2440
|
+
for (const item of pending) {
|
|
2441
|
+
embeddings.push(await embedText(item.text, userId, deps));
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
for (const [index, item] of pending.entries()) {
|
|
2445
|
+
const embedding = embeddings[index];
|
|
2446
|
+
if (embedding) generatedEmbeddings.set(item.key, embedding);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
for (const [messageIndex, plan] of plans.entries()) {
|
|
2450
|
+
const incomingMessage = plan.message;
|
|
2451
|
+
const pieces = plan.pieces;
|
|
2452
|
+
let message = incomingMessage;
|
|
2453
|
+
for (const piece of pieces) {
|
|
2454
|
+
const contentHash = sha256(piece.content);
|
|
2455
|
+
const canReuseParentEmbedding = pieces.length === 1 && isUsableEmbedding(incomingMessage.embedding);
|
|
2456
|
+
const embedding = canReuseParentEmbedding ? incomingMessage.embedding : generatedEmbeddings.get(`${messageIndex}:${piece.chunkIndex}`);
|
|
2457
|
+
generatedEmbedding = generatedEmbedding || Boolean(embedding && !canReuseParentEmbedding);
|
|
2458
|
+
if (!embedding) missingSemanticEmbedding = true;
|
|
2459
|
+
const embeddingModel = embedding ? canReuseParentEmbedding ? incomingMessage.embeddingModel : deps.embeddingInfo?.model ?? incomingMessage.embeddingModel ?? "server" : void 0;
|
|
2460
|
+
chunks.push({
|
|
2461
|
+
chunkId: `${incomingMessage.messageId}:chunk:${piece.chunkIndex}:${contentHash.slice(0, 16)}`,
|
|
2462
|
+
messageId: incomingMessage.messageId,
|
|
2463
|
+
userId: incomingMessage.userId,
|
|
2464
|
+
chunkIndex: piece.chunkIndex,
|
|
2465
|
+
chunkCount: pieces.length,
|
|
2466
|
+
startPosition: piece.startPosition,
|
|
2467
|
+
endPosition: piece.endPosition,
|
|
2468
|
+
content: piece.content,
|
|
2469
|
+
contentHash,
|
|
2470
|
+
embedding,
|
|
2471
|
+
embeddingModel,
|
|
2472
|
+
embeddingDimensions: embedding?.length,
|
|
2473
|
+
embeddingUpdatedAt: embedding ? Date.now() : void 0
|
|
2474
|
+
});
|
|
2475
|
+
if (pieces.length === 1 && embedding && !incomingMessage.embedding?.length) {
|
|
2476
|
+
message = {
|
|
2477
|
+
...incomingMessage,
|
|
2478
|
+
embedding,
|
|
2479
|
+
embeddingModel,
|
|
2480
|
+
embeddingContentHash: contentHash,
|
|
2481
|
+
embeddingDimensions: embedding.length,
|
|
2482
|
+
embeddingUpdatedAt: Date.now()
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
messages.push(message);
|
|
2487
|
+
}
|
|
2488
|
+
const warnings = [];
|
|
2489
|
+
if (generatedEmbedding && embedOnInsert !== true) warnings.push(AUTO_APPLIED_WARNING);
|
|
2490
|
+
if (missingSemanticEmbedding) warnings.push(SEMANTIC_UNAVAILABLE_WARNING);
|
|
2491
|
+
return { messages, chunks, warnings };
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
// src/storage/raw-message-ingest.ts
|
|
2495
|
+
async function persistRawMessages(input) {
|
|
2496
|
+
const prepared = await prepareRawMessageIngest(input.messages, input.embedOnInsert, input.unified);
|
|
2497
|
+
let ids;
|
|
2498
|
+
if (typeof input.manager.storeMessagesWithSearchChunks === "function") {
|
|
2499
|
+
ids = await input.manager.storeMessagesWithSearchChunks(prepared.messages, prepared.chunks);
|
|
2500
|
+
} else {
|
|
2501
|
+
const legacyMessages = (input.embedOnInsert === true || typeof input.unified?.embedQuery === "function") && typeof input.unified?.embedQuery === "function" ? await embedMissingMessages(prepared.messages, input.unified) : prepared.messages;
|
|
2502
|
+
if (typeof input.manager.upsertRawMessages === "function") {
|
|
2503
|
+
await input.manager.upsertRawMessages({ userId: input.userId, messages: legacyMessages });
|
|
2504
|
+
} else if (typeof input.manager.storeMessages === "function") {
|
|
2505
|
+
ids = await input.manager.storeMessages(legacyMessages);
|
|
2506
|
+
} else {
|
|
2507
|
+
throw new Error("active raw-message manager exposes no supported write method");
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
if (input.externalIndex) {
|
|
2511
|
+
await input.externalIndex.replaceMessages(prepared.messages, prepared.chunks);
|
|
2512
|
+
}
|
|
2513
|
+
return { count: prepared.messages.length, ids, warnings: prepared.warnings };
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2176
2516
|
// src/mcp.ts
|
|
2177
|
-
init_chroma_memory_index();
|
|
2178
2517
|
init_raw_message_store();
|
|
2179
2518
|
init_sqlite_raw_message_store();
|
|
2180
2519
|
var DEFAULT_NAME = "@melandlabs/memory-store";
|
|
@@ -2187,13 +2526,37 @@ async function startMcpServer(options = {}) {
|
|
|
2187
2526
|
const rawStore = createRawMessageStore({
|
|
2188
2527
|
env: options.env
|
|
2189
2528
|
});
|
|
2190
|
-
const
|
|
2529
|
+
const rawMessageManager = await rawStore.getManager();
|
|
2530
|
+
const search = createUnifiedSearch({
|
|
2531
|
+
...options.unified,
|
|
2532
|
+
searchRawMessagesLexical: async (input) => {
|
|
2533
|
+
if (typeof rawMessageManager.lexicalSearchMessages !== "function") return [];
|
|
2534
|
+
const results = await rawMessageManager.lexicalSearchMessages(input);
|
|
2535
|
+
return results.filter(Boolean).map((result) => ({
|
|
2536
|
+
id: result.id,
|
|
2537
|
+
content: result.content,
|
|
2538
|
+
similarity: result.similarity,
|
|
2539
|
+
metadata: result.metadata ?? {}
|
|
2540
|
+
}));
|
|
2541
|
+
}
|
|
2542
|
+
});
|
|
2191
2543
|
const vsaDbPath = resolveSQLiteRawMessageDbPath(options.dbPath);
|
|
2192
2544
|
const vsaStorage = await getSQLiteVsaStore({ dbPath: vsaDbPath });
|
|
2193
2545
|
const vsa = createVsaRecall(vsaStorage);
|
|
2194
|
-
server.tool("memory.health", "Returns the health status of the memory store.", async () =>
|
|
2195
|
-
|
|
2196
|
-
|
|
2546
|
+
server.tool("memory.health", "Returns the health status of the memory store.", async () => {
|
|
2547
|
+
const retrieval = options.unified?.getRawMessageRetrievalStatus ? await options.unified.getRawMessageRetrievalStatus().catch(() => ({
|
|
2548
|
+
backend: "unknown",
|
|
2549
|
+
childCount: 0,
|
|
2550
|
+
embeddedChildCount: 0,
|
|
2551
|
+
indexedDimensions: [],
|
|
2552
|
+
semanticReady: false,
|
|
2553
|
+
lexicalReady: false,
|
|
2554
|
+
semanticDegradedReason: "health_check_failed"
|
|
2555
|
+
})) : void 0;
|
|
2556
|
+
return {
|
|
2557
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, retrieval }) }]
|
|
2558
|
+
};
|
|
2559
|
+
});
|
|
2197
2560
|
const searchSchema = {
|
|
2198
2561
|
userId: z.string(),
|
|
2199
2562
|
query: z.string().min(1),
|
|
@@ -2211,7 +2574,11 @@ async function startMcpServer(options = {}) {
|
|
|
2211
2574
|
// coerces it into `synthesize.responseSchema` when present.
|
|
2212
2575
|
responseSchema: z.record(z.string(), z.unknown()).optional(),
|
|
2213
2576
|
limit: z.number().int().min(1).max(50).default(10),
|
|
2214
|
-
threshold: z.number().min(-1).max(1).
|
|
2577
|
+
threshold: z.number().min(-1).max(1).optional().describe(
|
|
2578
|
+
"Optional similarity floor. Omit it to let the default RRF pipeline fuse the full candidate window."
|
|
2579
|
+
),
|
|
2580
|
+
mergeStrategy: z.enum(["rrf", "similarity"]).optional(),
|
|
2581
|
+
includeRetrievalDiagnostics: z.boolean().optional(),
|
|
2215
2582
|
botIds: z.array(z.string()).optional(),
|
|
2216
2583
|
documentIds: z.array(z.string()).optional(),
|
|
2217
2584
|
includeArchivedInsights: z.boolean().default(false),
|
|
@@ -2275,6 +2642,8 @@ async function startMcpServer(options = {}) {
|
|
|
2275
2642
|
sources: a.sources,
|
|
2276
2643
|
limit: a.limit,
|
|
2277
2644
|
threshold: a.threshold,
|
|
2645
|
+
mergeStrategy: a.mergeStrategy,
|
|
2646
|
+
includeRetrievalDiagnostics: a.includeRetrievalDiagnostics,
|
|
2278
2647
|
botIds: a.botIds,
|
|
2279
2648
|
documentIds: a.documentIds,
|
|
2280
2649
|
authToken: a.authToken,
|
|
@@ -2305,37 +2674,24 @@ async function startMcpServer(options = {}) {
|
|
|
2305
2674
|
const a = args;
|
|
2306
2675
|
const manager2 = await rawStore.getManager();
|
|
2307
2676
|
const incoming = [a.message].map((m) => ({ ...m, userId: m.userId ?? a.userId }));
|
|
2308
|
-
const
|
|
2309
|
-
|
|
2310
|
-
a.
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
messages
|
|
2318
|
-
});
|
|
2319
|
-
} else if (typeof manager2.storeMessages === "function") {
|
|
2320
|
-
const ids = await manager2.storeMessages(messages);
|
|
2321
|
-
result = { inserted: ids.length, ids };
|
|
2322
|
-
} else {
|
|
2323
|
-
throw new Error("active raw-message manager exposes neither upsertRawMessages nor storeMessages");
|
|
2324
|
-
}
|
|
2325
|
-
try {
|
|
2326
|
-
await upsertRawMessagesToChroma(messages);
|
|
2327
|
-
} catch (error) {
|
|
2328
|
-
console.warn("[memory-store/mcp] chroma upsert failed:", error);
|
|
2329
|
-
}
|
|
2677
|
+
const persisted = await persistRawMessages({
|
|
2678
|
+
manager: manager2,
|
|
2679
|
+
userId: a.userId,
|
|
2680
|
+
messages: incoming,
|
|
2681
|
+
embedOnInsert: a.embedOnInsert,
|
|
2682
|
+
unified: options.unified,
|
|
2683
|
+
externalIndex: options.unified?.rawMessageChildIndex
|
|
2684
|
+
});
|
|
2685
|
+
const result = persisted.ids ? { inserted: persisted.ids.length, ids: persisted.ids } : { inserted: persisted.count };
|
|
2330
2686
|
return {
|
|
2331
2687
|
content: [
|
|
2332
2688
|
{
|
|
2333
2689
|
type: "text",
|
|
2334
2690
|
text: JSON.stringify({
|
|
2335
2691
|
ok: true,
|
|
2336
|
-
count:
|
|
2692
|
+
count: persisted.count,
|
|
2337
2693
|
result,
|
|
2338
|
-
...warnings.length > 0 ? { warnings } : {}
|
|
2694
|
+
...persisted.warnings.length > 0 ? { warnings: persisted.warnings } : {}
|
|
2339
2695
|
})
|
|
2340
2696
|
}
|
|
2341
2697
|
]
|
|
@@ -2675,7 +3031,17 @@ async function startMcpServer(options = {}) {
|
|
|
2675
3031
|
);
|
|
2676
3032
|
const okfMcpSpecifier = "@melandlabs/okf/mcp";
|
|
2677
3033
|
const { registerOkfTools } = await import(okfMcpSpecifier);
|
|
2678
|
-
registerOkfTools(server, rawStore
|
|
3034
|
+
registerOkfTools(server, rawStore, {
|
|
3035
|
+
writeMessages: async (userId, messages) => {
|
|
3036
|
+
await persistRawMessages({
|
|
3037
|
+
manager: rawMessageManager,
|
|
3038
|
+
userId,
|
|
3039
|
+
messages,
|
|
3040
|
+
unified: options.unified,
|
|
3041
|
+
externalIndex: options.unified?.rawMessageChildIndex
|
|
3042
|
+
});
|
|
3043
|
+
}
|
|
3044
|
+
});
|
|
2679
3045
|
const transport = new StdioServerTransport();
|
|
2680
3046
|
await server.connect(transport);
|
|
2681
3047
|
return server;
|
|
@@ -3020,6 +3386,121 @@ function createUserVoiceRewriter(options) {
|
|
|
3020
3386
|
};
|
|
3021
3387
|
}
|
|
3022
3388
|
|
|
3389
|
+
// src/storage/raw-message-child-vector-index.ts
|
|
3390
|
+
var RawMessageChildVectorIndex = class {
|
|
3391
|
+
backend;
|
|
3392
|
+
store;
|
|
3393
|
+
catalog;
|
|
3394
|
+
constructor(options) {
|
|
3395
|
+
this.backend = options.backend;
|
|
3396
|
+
this.store = options.store;
|
|
3397
|
+
this.catalog = options.catalog;
|
|
3398
|
+
}
|
|
3399
|
+
async replaceMessages(messages, chunks) {
|
|
3400
|
+
const chunksByMessage = /* @__PURE__ */ new Map();
|
|
3401
|
+
for (const chunk of chunks) {
|
|
3402
|
+
const group = chunksByMessage.get(chunk.messageId) ?? [];
|
|
3403
|
+
group.push(chunk);
|
|
3404
|
+
chunksByMessage.set(chunk.messageId, group);
|
|
3405
|
+
}
|
|
3406
|
+
for (const message of messages) {
|
|
3407
|
+
await this.store.deleteDocument(message.messageId);
|
|
3408
|
+
const vectorChunks = (chunksByMessage.get(message.messageId) ?? []).filter(
|
|
3409
|
+
(chunk) => Boolean(chunk.embedding?.length) && !chunk.embedding?.every((value) => value === 0)
|
|
3410
|
+
).map((chunk) => ({
|
|
3411
|
+
id: chunk.chunkId,
|
|
3412
|
+
documentId: message.messageId,
|
|
3413
|
+
content: chunk.content,
|
|
3414
|
+
embedding: chunk.embedding,
|
|
3415
|
+
metadata: {
|
|
3416
|
+
userId: message.userId,
|
|
3417
|
+
botId: message.botId,
|
|
3418
|
+
platform: message.platform,
|
|
3419
|
+
timestamp: message.timestamp,
|
|
3420
|
+
chunkIndex: chunk.chunkIndex,
|
|
3421
|
+
chunkCount: chunk.chunkCount,
|
|
3422
|
+
startPosition: chunk.startPosition,
|
|
3423
|
+
endPosition: chunk.endPosition,
|
|
3424
|
+
contentHash: chunk.contentHash,
|
|
3425
|
+
embeddingModel: chunk.embeddingModel
|
|
3426
|
+
}
|
|
3427
|
+
}));
|
|
3428
|
+
if (vectorChunks.length > 0) await this.store.addChunks(vectorChunks);
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
async search(input) {
|
|
3432
|
+
const results = input.hybrid && this.store.hybridSearch ? await this.store.hybridSearch({
|
|
3433
|
+
text: input.query ?? "",
|
|
3434
|
+
vector: input.queryEmbedding,
|
|
3435
|
+
limit: input.limit,
|
|
3436
|
+
candidateLimit: input.limit,
|
|
3437
|
+
fusion: "rrf",
|
|
3438
|
+
filter: { userId: input.userId }
|
|
3439
|
+
}) : await this.store.similaritySearch(input.queryEmbedding, input.limit, input.userId);
|
|
3440
|
+
return this.hydrate(results, input);
|
|
3441
|
+
}
|
|
3442
|
+
async getChunkCount() {
|
|
3443
|
+
return this.store.getChunkCount();
|
|
3444
|
+
}
|
|
3445
|
+
async hydrate(results, input) {
|
|
3446
|
+
const matchedChunks = await this.catalog.getRawMessageSearchChunks({
|
|
3447
|
+
chunkIds: results.map((result) => result.id),
|
|
3448
|
+
userId: input.userId
|
|
3449
|
+
});
|
|
3450
|
+
const matchedById = new Map(matchedChunks.map((chunk) => [chunk.chunkId, chunk]));
|
|
3451
|
+
const messageIds = [...new Set(matchedChunks.map((chunk) => chunk.messageId))];
|
|
3452
|
+
const [allChunks, parents] = await Promise.all([
|
|
3453
|
+
this.catalog.getRawMessageSearchChunks({ messageIds, userId: input.userId }),
|
|
3454
|
+
Promise.all(messageIds.map((messageId) => this.catalog.getMessageById(messageId)))
|
|
3455
|
+
]);
|
|
3456
|
+
const parentById = new Map(
|
|
3457
|
+
parents.filter((parent) => Boolean(parent)).map((parent) => [parent.messageId, parent])
|
|
3458
|
+
);
|
|
3459
|
+
const chunksByMessage = /* @__PURE__ */ new Map();
|
|
3460
|
+
for (const chunk of allChunks) {
|
|
3461
|
+
const group = chunksByMessage.get(chunk.messageId) ?? [];
|
|
3462
|
+
group.push(chunk);
|
|
3463
|
+
chunksByMessage.set(chunk.messageId, group);
|
|
3464
|
+
}
|
|
3465
|
+
const strongest = /* @__PURE__ */ new Map();
|
|
3466
|
+
for (const result of results) {
|
|
3467
|
+
const chunk = matchedById.get(result.id);
|
|
3468
|
+
if (!chunk) continue;
|
|
3469
|
+
const parent = parentById.get(chunk.messageId);
|
|
3470
|
+
if (!parent || input.botId && parent.botId !== input.botId) continue;
|
|
3471
|
+
if (input.threshold !== void 0 && result.score < input.threshold) continue;
|
|
3472
|
+
const siblings = (chunksByMessage.get(parent.messageId) ?? []).sort(
|
|
3473
|
+
(a, b) => a.chunkIndex - b.chunkIndex
|
|
3474
|
+
);
|
|
3475
|
+
const content = chunk.chunkCount <= 1 ? parent.content : parent.content.slice(
|
|
3476
|
+
siblings[Math.max(0, chunk.chunkIndex - 1)]?.startPosition ?? chunk.startPosition,
|
|
3477
|
+
siblings[Math.min(siblings.length - 1, chunk.chunkIndex + 1)]?.endPosition ?? chunk.endPosition
|
|
3478
|
+
);
|
|
3479
|
+
const hit = {
|
|
3480
|
+
id: parent.messageId,
|
|
3481
|
+
content,
|
|
3482
|
+
similarity: result.score,
|
|
3483
|
+
metadata: {
|
|
3484
|
+
...parent.metadata ?? {},
|
|
3485
|
+
userId: parent.userId,
|
|
3486
|
+
botId: parent.botId,
|
|
3487
|
+
platform: parent.platform,
|
|
3488
|
+
timestamp: parent.timestamp,
|
|
3489
|
+
sourceMessageId: parent.messageId,
|
|
3490
|
+
sourceChunkId: chunk.chunkId,
|
|
3491
|
+
sourceChunkIndex: chunk.chunkIndex,
|
|
3492
|
+
sourceChunkCount: chunk.chunkCount,
|
|
3493
|
+
backend: this.backend,
|
|
3494
|
+
scoring: input.hybrid ? "native-hybrid" : "dense"
|
|
3495
|
+
}
|
|
3496
|
+
};
|
|
3497
|
+
const existing = strongest.get(parent.messageId);
|
|
3498
|
+
if (!existing || hit.similarity > existing.similarity) strongest.set(parent.messageId, hit);
|
|
3499
|
+
}
|
|
3500
|
+
return [...strongest.values()].sort((a, b) => b.similarity - a.similarity).slice(0, input.limit);
|
|
3501
|
+
}
|
|
3502
|
+
};
|
|
3503
|
+
|
|
3023
3504
|
// src/server/cli-shared.ts
|
|
3024
3505
|
init_raw_message_store();
|
|
3025
3506
|
|
|
@@ -3034,7 +3515,7 @@ function isInsightSQLiteVecEnabled(_env) {
|
|
|
3034
3515
|
}
|
|
3035
3516
|
async function getInsightSQLiteVecStore(_env) {
|
|
3036
3517
|
const dbPath = resolveSQLiteRawMessageDbPath();
|
|
3037
|
-
const { getSQLiteVecStore } = await import('@melandlabs/rag/sqlite-vec-store');
|
|
3518
|
+
const { getSQLiteVecStore } = await import('@melandlabs/ai-rag/sqlite-vec-store');
|
|
3038
3519
|
return await getSQLiteVecStore(dbPath, void 0, {
|
|
3039
3520
|
collectionName: process.env.SQLITE_VEC_INSIGHTS_COLLECTION || "opencontext_insights"
|
|
3040
3521
|
});
|
|
@@ -3058,17 +3539,19 @@ async function searchInsightsWithSQLiteVec(input, env) {
|
|
|
3058
3539
|
// src/server/cli-shared.ts
|
|
3059
3540
|
async function loadAiRag() {
|
|
3060
3541
|
try {
|
|
3061
|
-
const [local, chroma] = await Promise.all([
|
|
3542
|
+
const [local, reranker, chroma] = await Promise.all([
|
|
3062
3543
|
import('@melandlabs/ai-rag/local-transformers-embedding-provider'),
|
|
3544
|
+
import('@melandlabs/ai-rag/local-transformers-reranker'),
|
|
3063
3545
|
import('@melandlabs/ai-rag/chroma-store')
|
|
3064
3546
|
]);
|
|
3065
3547
|
return {
|
|
3066
3548
|
LocalTransformersEmbeddingProvider: local.LocalTransformersEmbeddingProvider,
|
|
3549
|
+
LocalTransformersReranker: reranker.LocalTransformersReranker,
|
|
3067
3550
|
ChromaVectorStore: chroma.ChromaVectorStore
|
|
3068
3551
|
};
|
|
3069
3552
|
} catch (error) {
|
|
3070
3553
|
throw new Error(
|
|
3071
|
-
|
|
3554
|
+
`Local embedding/reranking and insight/knowledge Chroma require @melandlabs/ai-rag to be installed. Run \`pnpm add @melandlabs/ai-rag\` and try again. (${error.message})`
|
|
3072
3555
|
);
|
|
3073
3556
|
}
|
|
3074
3557
|
}
|
|
@@ -3077,8 +3560,21 @@ function parseUnifiedArgs(argv) {
|
|
|
3077
3560
|
const args = {
|
|
3078
3561
|
embeddingProvider: env.EMBEDDING_PROVIDER ?? "none",
|
|
3079
3562
|
embeddingModel: env.EMBEDDING_MODEL,
|
|
3563
|
+
embeddingCacheDir: env.LOCAL_EMBEDDING_CACHE_DIR,
|
|
3564
|
+
rerankerProvider: env.RERANKER_PROVIDER ?? "none",
|
|
3565
|
+
rerankerModel: env.LOCAL_RERANKER_MODEL,
|
|
3566
|
+
rerankerCacheDir: env.LOCAL_RERANKER_CACHE_DIR,
|
|
3567
|
+
rerankerBatchSize: env.LOCAL_RERANKER_BATCH_SIZE ? Number.parseInt(env.LOCAL_RERANKER_BATCH_SIZE, 10) : void 0,
|
|
3568
|
+
rerankerMaxTokens: env.LOCAL_RERANKER_MAX_TOKENS ? Number.parseInt(env.LOCAL_RERANKER_MAX_TOKENS, 10) : void 0,
|
|
3080
3569
|
chromaUrl: env.CHROMA_URL,
|
|
3081
3570
|
memoryBackend: env.MEMORY_BACKEND ?? "none",
|
|
3571
|
+
lancedbUri: env.LANCEDB_URI,
|
|
3572
|
+
lancedbTable: env.LANCEDB_TABLE,
|
|
3573
|
+
milvusAddress: env.MILVUS_ADDRESS,
|
|
3574
|
+
milvusToken: env.MILVUS_TOKEN,
|
|
3575
|
+
milvusDatabase: env.MILVUS_DATABASE,
|
|
3576
|
+
milvusCollection: env.MILVUS_COLLECTION,
|
|
3577
|
+
milvusDimension: env.MILVUS_DIMENSION ? Number.parseInt(env.MILVUS_DIMENSION, 10) : void 0,
|
|
3082
3578
|
insightsBackend: env.INSIGHTS_BACKEND ?? "none",
|
|
3083
3579
|
insightsCollection: env.INSIGHTS_COLLECTION ?? "opencontext_insights",
|
|
3084
3580
|
knowledgeBackend: env.KNOWLEDGE_BACKEND ?? "none",
|
|
@@ -3103,12 +3599,51 @@ function parseUnifiedArgs(argv) {
|
|
|
3103
3599
|
case "--embedding-model":
|
|
3104
3600
|
args.embeddingModel = takeValue();
|
|
3105
3601
|
break;
|
|
3602
|
+
case "--embedding-cache-dir":
|
|
3603
|
+
args.embeddingCacheDir = takeValue();
|
|
3604
|
+
break;
|
|
3605
|
+
case "--reranker-provider":
|
|
3606
|
+
args.rerankerProvider = takeValue();
|
|
3607
|
+
break;
|
|
3608
|
+
case "--reranker-model":
|
|
3609
|
+
args.rerankerModel = takeValue();
|
|
3610
|
+
break;
|
|
3611
|
+
case "--reranker-cache-dir":
|
|
3612
|
+
args.rerankerCacheDir = takeValue();
|
|
3613
|
+
break;
|
|
3614
|
+
case "--reranker-batch-size":
|
|
3615
|
+
args.rerankerBatchSize = Number.parseInt(takeValue(), 10);
|
|
3616
|
+
break;
|
|
3617
|
+
case "--reranker-max-tokens":
|
|
3618
|
+
args.rerankerMaxTokens = Number.parseInt(takeValue(), 10);
|
|
3619
|
+
break;
|
|
3106
3620
|
case "--chroma-url":
|
|
3107
3621
|
args.chromaUrl = takeValue();
|
|
3108
3622
|
break;
|
|
3109
3623
|
case "--memory-backend":
|
|
3110
3624
|
args.memoryBackend = takeValue();
|
|
3111
3625
|
break;
|
|
3626
|
+
case "--lancedb-uri":
|
|
3627
|
+
args.lancedbUri = takeValue();
|
|
3628
|
+
break;
|
|
3629
|
+
case "--lancedb-table":
|
|
3630
|
+
args.lancedbTable = takeValue();
|
|
3631
|
+
break;
|
|
3632
|
+
case "--milvus-address":
|
|
3633
|
+
args.milvusAddress = takeValue();
|
|
3634
|
+
break;
|
|
3635
|
+
case "--milvus-token":
|
|
3636
|
+
args.milvusToken = takeValue();
|
|
3637
|
+
break;
|
|
3638
|
+
case "--milvus-database":
|
|
3639
|
+
args.milvusDatabase = takeValue();
|
|
3640
|
+
break;
|
|
3641
|
+
case "--milvus-collection":
|
|
3642
|
+
args.milvusCollection = takeValue();
|
|
3643
|
+
break;
|
|
3644
|
+
case "--milvus-dimension":
|
|
3645
|
+
args.milvusDimension = Number.parseInt(takeValue(), 10);
|
|
3646
|
+
break;
|
|
3112
3647
|
case "--insights-backend":
|
|
3113
3648
|
args.insightsBackend = takeValue();
|
|
3114
3649
|
break;
|
|
@@ -3138,31 +3673,60 @@ function parseUnifiedArgs(argv) {
|
|
|
3138
3673
|
break;
|
|
3139
3674
|
}
|
|
3140
3675
|
}
|
|
3141
|
-
|
|
3142
|
-
["--embedding-provider", args.embeddingProvider],
|
|
3143
|
-
["--memory-backend", args.memoryBackend],
|
|
3144
|
-
["--insights-backend", args.insightsBackend],
|
|
3145
|
-
["--knowledge-backend", args.knowledgeBackend]
|
|
3146
|
-
]) {
|
|
3147
|
-
const allowed = ["local", "openrouter", "none", "chroma", "sqlite-vec"];
|
|
3676
|
+
const validate = (name, value, allowed) => {
|
|
3148
3677
|
if (!allowed.includes(value)) {
|
|
3149
3678
|
throw new Error(`[memory-store/cli] ${name} must be one of: ${allowed.join(", ")} (got "${value}")`);
|
|
3150
3679
|
}
|
|
3680
|
+
};
|
|
3681
|
+
validate("--embedding-provider", args.embeddingProvider, ["local", "openrouter", "none"]);
|
|
3682
|
+
validate("--reranker-provider", args.rerankerProvider, ["local", "none"]);
|
|
3683
|
+
validate("--memory-backend", args.memoryBackend, ["sqlite-vec", "chroma", "lancedb", "milvus", "none"]);
|
|
3684
|
+
validate("--insights-backend", args.insightsBackend, ["sqlite-vec", "chroma", "none"]);
|
|
3685
|
+
validate("--knowledge-backend", args.knowledgeBackend, ["chroma", "none"]);
|
|
3686
|
+
if (args.milvusDimension !== void 0 && (!Number.isInteger(args.milvusDimension) || args.milvusDimension <= 0)) {
|
|
3687
|
+
throw new Error("[memory-store/cli] --milvus-dimension must be a positive integer");
|
|
3688
|
+
}
|
|
3689
|
+
for (const [name, value] of [
|
|
3690
|
+
["--reranker-batch-size", args.rerankerBatchSize],
|
|
3691
|
+
["--reranker-max-tokens", args.rerankerMaxTokens]
|
|
3692
|
+
]) {
|
|
3693
|
+
if (value !== void 0 && (!Number.isInteger(value) || value <= 0)) {
|
|
3694
|
+
throw new Error(`[memory-store/cli] ${name} must be a positive integer`);
|
|
3695
|
+
}
|
|
3151
3696
|
}
|
|
3152
3697
|
return args;
|
|
3153
3698
|
}
|
|
3154
3699
|
async function buildUnified(args) {
|
|
3155
3700
|
const unified = {};
|
|
3156
3701
|
const log = (msg) => console.warn(`[memory-store/cli] ${msg}`);
|
|
3157
|
-
const wantsAiRag = args.embeddingProvider === "local" || args.
|
|
3702
|
+
const wantsAiRag = args.embeddingProvider === "local" || args.rerankerProvider === "local" || args.insightsBackend === "chroma" || args.knowledgeBackend === "chroma";
|
|
3158
3703
|
const aiRag = wantsAiRag ? await loadAiRag() : null;
|
|
3159
3704
|
if (args.embeddingProvider === "local") {
|
|
3160
3705
|
try {
|
|
3161
3706
|
if (!aiRag) {
|
|
3162
3707
|
throw new Error("ai-rag modules not loaded");
|
|
3163
3708
|
}
|
|
3164
|
-
const provider = new aiRag.LocalTransformersEmbeddingProvider({
|
|
3165
|
-
|
|
3709
|
+
const provider = new aiRag.LocalTransformersEmbeddingProvider({
|
|
3710
|
+
modelName: args.embeddingModel,
|
|
3711
|
+
cacheDir: args.embeddingCacheDir
|
|
3712
|
+
});
|
|
3713
|
+
unified.embeddingInfo = {
|
|
3714
|
+
provider: "local",
|
|
3715
|
+
model: args.embeddingModel ?? "Xenova/all-MiniLM-L6-v2",
|
|
3716
|
+
maxTokens: 512
|
|
3717
|
+
};
|
|
3718
|
+
unified.embedQuery = async ({ query }) => {
|
|
3719
|
+
const embedding = await provider.embedQuery(query);
|
|
3720
|
+
if (unified.embeddingInfo) unified.embeddingInfo.dimensions = embedding.length;
|
|
3721
|
+
return embedding;
|
|
3722
|
+
};
|
|
3723
|
+
unified.embedDocuments = async ({ texts }) => {
|
|
3724
|
+
const embeddings = await provider.embedDocuments(texts);
|
|
3725
|
+
if (unified.embeddingInfo && embeddings[0]) {
|
|
3726
|
+
unified.embeddingInfo.dimensions = embeddings[0].length;
|
|
3727
|
+
}
|
|
3728
|
+
return embeddings;
|
|
3729
|
+
};
|
|
3166
3730
|
log(
|
|
3167
3731
|
`embedQuery wired via LocalTransformersEmbeddingProvider (model=${args.embeddingModel ?? "Xenova/all-MiniLM-L6-v2"})`
|
|
3168
3732
|
);
|
|
@@ -3178,6 +3742,7 @@ async function buildUnified(args) {
|
|
|
3178
3742
|
}
|
|
3179
3743
|
const model = args.embeddingModel ?? "text-embedding-3-small";
|
|
3180
3744
|
const baseURL = "https://openrouter.ai/api/v1";
|
|
3745
|
+
unified.embeddingInfo = { provider: "openrouter", model };
|
|
3181
3746
|
unified.embedQuery = async ({ query }) => {
|
|
3182
3747
|
const res = await fetch(`${baseURL}/embeddings`, {
|
|
3183
3748
|
method: "POST",
|
|
@@ -3195,49 +3760,150 @@ async function buildUnified(args) {
|
|
|
3195
3760
|
const body = await res.json();
|
|
3196
3761
|
const embedding = body.data?.[0]?.embedding;
|
|
3197
3762
|
if (!embedding) throw new Error("openrouter embeddings response missing data[0].embedding");
|
|
3763
|
+
if (unified.embeddingInfo) unified.embeddingInfo.dimensions = embedding.length;
|
|
3198
3764
|
return embedding;
|
|
3199
3765
|
};
|
|
3200
3766
|
log(`embedQuery wired via openrouter (model=${model})`);
|
|
3201
3767
|
}
|
|
3202
|
-
if (args.
|
|
3203
|
-
|
|
3204
|
-
const
|
|
3205
|
-
|
|
3768
|
+
if (args.rerankerProvider === "local") {
|
|
3769
|
+
if (!aiRag) throw new Error("Local reranker modules were not loaded");
|
|
3770
|
+
const reranker = new aiRag.LocalTransformersReranker({
|
|
3771
|
+
modelName: args.rerankerModel,
|
|
3772
|
+
cacheDir: args.rerankerCacheDir,
|
|
3773
|
+
batchSize: args.rerankerBatchSize,
|
|
3774
|
+
maxTokens: args.rerankerMaxTokens
|
|
3775
|
+
});
|
|
3776
|
+
await reranker.warmup();
|
|
3777
|
+
unified.reranker = reranker;
|
|
3778
|
+
unified.rerankerInfo = {
|
|
3779
|
+
provider: "local",
|
|
3780
|
+
model: reranker.getModelName(),
|
|
3781
|
+
maxTokens: reranker.getMaxTokens(),
|
|
3782
|
+
ready: true
|
|
3783
|
+
};
|
|
3784
|
+
log(`reranker wired via local Transformers.js (model=${reranker.getModelName()})`);
|
|
3785
|
+
} else {
|
|
3786
|
+
unified.rerankerInfo = { provider: "none", ready: false };
|
|
3787
|
+
}
|
|
3788
|
+
if (args.memoryBackend !== "none") {
|
|
3789
|
+
if (args.memoryBackend === "chroma" && !args.chromaUrl) {
|
|
3790
|
+
throw new Error("--memory-backend=chroma requires --chroma-url <url> (or CHROMA_URL env)");
|
|
3791
|
+
}
|
|
3792
|
+
if (args.memoryBackend === "lancedb" && !args.lancedbUri) {
|
|
3793
|
+
throw new Error("--memory-backend=lancedb requires --lancedb-uri <uri> (or LANCEDB_URI env)");
|
|
3794
|
+
}
|
|
3795
|
+
if (args.memoryBackend === "milvus" && !args.milvusAddress) {
|
|
3796
|
+
throw new Error("--memory-backend=milvus requires --milvus-address <address> (or MILVUS_ADDRESS env)");
|
|
3797
|
+
}
|
|
3798
|
+
if (args.memoryBackend === "lancedb") {
|
|
3799
|
+
try {
|
|
3800
|
+
await import('@lancedb/lancedb');
|
|
3801
|
+
} catch (error) {
|
|
3802
|
+
throw new Error(
|
|
3803
|
+
`LanceDB requires the optional dependency @lancedb/lancedb (${error.message})`
|
|
3804
|
+
);
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
if (args.memoryBackend === "milvus") {
|
|
3808
|
+
try {
|
|
3809
|
+
await import('@zilliz/milvus2-sdk-node');
|
|
3810
|
+
} catch (error) {
|
|
3811
|
+
throw new Error(
|
|
3812
|
+
`Milvus requires the optional dependency @zilliz/milvus2-sdk-node (${error.message})`
|
|
3813
|
+
);
|
|
3814
|
+
}
|
|
3815
|
+
}
|
|
3816
|
+
const rawStore = createRawMessageStore({ env: void 0 });
|
|
3817
|
+
const manager2 = await rawStore.getManager();
|
|
3818
|
+
if (typeof manager2.getRawMessageSearchChunks !== "function" || typeof manager2.getRawMessageSearchIndexStats !== "function") {
|
|
3206
3819
|
throw new Error(
|
|
3207
|
-
|
|
3820
|
+
`${args.memoryBackend} child retrieval requires the SQLite raw-message catalog; the Postgres manager remains on the legacy parent fallback in this release`
|
|
3208
3821
|
);
|
|
3209
3822
|
}
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3823
|
+
if (typeof manager2.lexicalSearchMessages === "function") {
|
|
3824
|
+
unified.searchRawMessagesLexical = async (input) => {
|
|
3825
|
+
const rows = await manager2.lexicalSearchMessages(input);
|
|
3826
|
+
return rows.map((row) => ({ ...row, metadata: row.metadata ?? {} }));
|
|
3827
|
+
};
|
|
3828
|
+
}
|
|
3829
|
+
let externalStore;
|
|
3830
|
+
if (args.memoryBackend === "chroma") {
|
|
3831
|
+
externalStore = new ChromaVectorStore({
|
|
3832
|
+
url: args.chromaUrl,
|
|
3833
|
+
collectionName: "opencontext_raw_message_chunks"
|
|
3834
|
+
});
|
|
3835
|
+
} else if (args.memoryBackend === "lancedb") {
|
|
3836
|
+
const { LanceDBStore } = await import('@melandlabs/rag/lancedb-store');
|
|
3837
|
+
externalStore = new LanceDBStore({
|
|
3838
|
+
uri: args.lancedbUri,
|
|
3839
|
+
tableName: args.lancedbTable ?? "opencontext_raw_message_chunks",
|
|
3840
|
+
defaultFusion: "rrf",
|
|
3841
|
+
candidateMultiplier: 4
|
|
3842
|
+
});
|
|
3843
|
+
} else if (args.memoryBackend === "milvus") {
|
|
3844
|
+
const { MilvusStore } = await import('@melandlabs/rag/milvus-store');
|
|
3845
|
+
externalStore = new MilvusStore({
|
|
3846
|
+
address: args.milvusAddress,
|
|
3847
|
+
token: args.milvusToken,
|
|
3848
|
+
database: args.milvusDatabase,
|
|
3849
|
+
collectionName: args.milvusCollection ?? "opencontext_raw_message_chunks",
|
|
3850
|
+
dimension: args.milvusDimension,
|
|
3851
|
+
defaultFusion: "rrf",
|
|
3852
|
+
candidateMultiplier: 4
|
|
3217
3853
|
});
|
|
3218
|
-
return rows.map((r) => ({
|
|
3219
|
-
id: r.id,
|
|
3220
|
-
content: r.content,
|
|
3221
|
-
similarity: r.similarity,
|
|
3222
|
-
metadata: r.metadata ?? {}
|
|
3223
|
-
}));
|
|
3224
|
-
};
|
|
3225
|
-
log("memory backend wired via sqlite-vec (uses the manager's searchMessagesSemantically)");
|
|
3226
|
-
} else if (args.memoryBackend === "chroma") {
|
|
3227
|
-
if (!args.chromaUrl)
|
|
3228
|
-
throw new Error("--memory-backend=chroma requires --chroma-url <url> (or CHROMA_URL env)");
|
|
3229
|
-
if (!aiRag) {
|
|
3230
|
-
throw new Error("ai-rag modules not loaded");
|
|
3231
3854
|
}
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3855
|
+
if (args.memoryBackend === "sqlite-vec") {
|
|
3856
|
+
if (typeof manager2.searchMessagesSemantically !== "function") {
|
|
3857
|
+
throw new Error("sqlite-vec child search is unavailable in the active raw-message manager");
|
|
3858
|
+
}
|
|
3859
|
+
unified.searchRawMessagesAnn = async ({ userId, queryEmbedding, limit, threshold, botId }) => {
|
|
3860
|
+
const rows = await manager2.searchMessagesSemantically({
|
|
3861
|
+
userId,
|
|
3862
|
+
queryEmbedding,
|
|
3863
|
+
limit,
|
|
3864
|
+
threshold,
|
|
3865
|
+
botId
|
|
3866
|
+
});
|
|
3867
|
+
return rows.map((row) => ({ ...row, metadata: row.metadata ?? {} }));
|
|
3868
|
+
};
|
|
3869
|
+
} else if (externalStore) {
|
|
3870
|
+
const childIndex = new RawMessageChildVectorIndex({
|
|
3871
|
+
backend: args.memoryBackend,
|
|
3872
|
+
store: externalStore,
|
|
3873
|
+
catalog: {
|
|
3874
|
+
getMessageById: (messageId) => manager2.getMessageById(messageId),
|
|
3875
|
+
// biome-ignore lint/style/noNonNullAssertion: guarded above
|
|
3876
|
+
getRawMessageSearchChunks: (input) => manager2.getRawMessageSearchChunks(input)
|
|
3877
|
+
}
|
|
3878
|
+
});
|
|
3879
|
+
unified.rawMessageChildIndex = childIndex;
|
|
3880
|
+
unified.searchRawMessagesAnn = (input) => childIndex.search(input);
|
|
3881
|
+
if (args.memoryBackend !== "chroma") {
|
|
3882
|
+
unified.searchRawMessagesHybrid = (input) => childIndex.search({ ...input, hybrid: true });
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
unified.getRawMessageRetrievalStatus = async () => {
|
|
3886
|
+
const stats = await manager2.getRawMessageSearchIndexStats();
|
|
3887
|
+
const embeddingDimensions = unified.embeddingInfo?.dimensions;
|
|
3888
|
+
const dimensionMismatch = embeddingDimensions !== void 0 && stats.embeddingDimensions.length > 0 && !stats.embeddingDimensions.includes(embeddingDimensions);
|
|
3889
|
+
const semanticReady = Boolean(unified.embedQuery) && (args.memoryBackend === "sqlite-vec" ? stats.semanticReady : stats.embeddedChunkCount > 0) && !dimensionMismatch;
|
|
3890
|
+
return {
|
|
3891
|
+
backend: args.memoryBackend,
|
|
3892
|
+
embeddingProvider: unified.embeddingInfo?.provider,
|
|
3893
|
+
embeddingModel: unified.embeddingInfo?.model,
|
|
3894
|
+
embeddingDimensions,
|
|
3895
|
+
childCount: stats.chunkCount,
|
|
3896
|
+
embeddedChildCount: stats.embeddedChunkCount,
|
|
3897
|
+
indexedDimensions: stats.embeddingDimensions,
|
|
3898
|
+
semanticReady,
|
|
3899
|
+
lexicalReady: stats.lexicalReady,
|
|
3900
|
+
semanticDegradedReason: dimensionMismatch ? "semantic_dimension_mismatch" : !unified.embedQuery ? "semantic_unavailable" : stats.embeddedChunkCount === 0 ? "semantic_child_vectors_empty" : void 0,
|
|
3901
|
+
rerankerProvider: unified.rerankerInfo?.provider,
|
|
3902
|
+
rerankerModel: unified.rerankerInfo?.model,
|
|
3903
|
+
rerankerReady: unified.rerankerInfo?.ready ?? false
|
|
3904
|
+
};
|
|
3239
3905
|
};
|
|
3240
|
-
log(
|
|
3906
|
+
log(`memory backend wired via ${args.memoryBackend} with SQLite child catalog and lexical search`);
|
|
3241
3907
|
}
|
|
3242
3908
|
if (args.insightsBackend === "sqlite-vec" && isInsightSQLiteVecEnabled()) {
|
|
3243
3909
|
if (!unified.embedQuery) {
|
|
@@ -3364,20 +4030,41 @@ function printUnifiedHelp() {
|
|
|
3364
4030
|
--embedding-model <name> Model name
|
|
3365
4031
|
(env: EMBEDDING_MODEL; local \u2192 Xenova/all-MiniLM-L6-v2,
|
|
3366
4032
|
openrouter \u2192 text-embedding-3-small)
|
|
4033
|
+
--embedding-cache-dir <path> Local embedding cache (env: LOCAL_EMBEDDING_CACHE_DIR)
|
|
3367
4034
|
Note: "local" dynamically imports @melandlabs/ai-rag (a peer install).
|
|
3368
4035
|
|
|
4036
|
+
Reranking (after RRF/source fusion, before final Top-K):
|
|
4037
|
+
--reranker-provider <name> local | none
|
|
4038
|
+
(env: RERANKER_PROVIDER, default: none)
|
|
4039
|
+
--reranker-model <name> Local sequence-classification model
|
|
4040
|
+
(env: LOCAL_RERANKER_MODEL,
|
|
4041
|
+
default: Xenova/ms-marco-MiniLM-L-6-v2)
|
|
4042
|
+
--reranker-cache-dir <path> Persistent model cache (env: LOCAL_RERANKER_CACHE_DIR)
|
|
4043
|
+
--reranker-batch-size <int> Pair scoring batch size (env: LOCAL_RERANKER_BATCH_SIZE,
|
|
4044
|
+
default: 8)
|
|
4045
|
+
--reranker-max-tokens <int> Query/document pair token limit
|
|
4046
|
+
(env: LOCAL_RERANKER_MAX_TOKENS, default: 512)
|
|
4047
|
+
|
|
3369
4048
|
Cross-source search (wires unified.searchKnowledge / searchInsights / searchRawMessagesAnn):
|
|
3370
4049
|
--chroma-url <url> Chroma server URL
|
|
3371
4050
|
(env: CHROMA_URL; required when any *-backend=chroma)
|
|
3372
|
-
--memory-backend <name> sqlite-vec | chroma | none
|
|
4051
|
+
--memory-backend <name> sqlite-vec | chroma | lancedb | milvus | none
|
|
3373
4052
|
(env: MEMORY_BACKEND, default: none)
|
|
4053
|
+
--lancedb-uri <uri> LanceDB directory or URI (env: LANCEDB_URI)
|
|
4054
|
+
--lancedb-table <name> Optional table name (env: LANCEDB_TABLE)
|
|
4055
|
+
--milvus-address <address> Milvus endpoint (env: MILVUS_ADDRESS)
|
|
4056
|
+
--milvus-token <token> Optional Milvus token (env: MILVUS_TOKEN)
|
|
4057
|
+
--milvus-database <name> Optional database (env: MILVUS_DATABASE)
|
|
4058
|
+
--milvus-collection <name> Optional collection (env: MILVUS_COLLECTION)
|
|
4059
|
+
--milvus-dimension <int> Optional vector dimension (env: MILVUS_DIMENSION)
|
|
3374
4060
|
--insights-backend <name> sqlite-vec | chroma | none
|
|
3375
4061
|
(env: INSIGHTS_BACKEND, default: none)
|
|
3376
4062
|
--insights-collection <name> Chroma collection (default: opencontext_insights)
|
|
3377
4063
|
--knowledge-backend <name> chroma | none
|
|
3378
4064
|
(env: KNOWLEDGE_BACKEND, default: none)
|
|
3379
4065
|
--knowledge-collection <name> Chroma collection (default: opencontext_knowledge)
|
|
3380
|
-
|
|
4066
|
+
Notes: memory Chroma uses @melandlabs/rag; insight/knowledge Chroma use
|
|
4067
|
+
@melandlabs/ai-rag. LanceDB and Milvus require their optional peer dependencies.
|
|
3381
4068
|
|
|
3382
4069
|
Reasoning (wires unified.reasoning.{queryRewriter, iterativePlanner}):
|
|
3383
4070
|
--reasoning Enable the LLM reasoning layer so /v1/search and
|