@melandlabs/memory-store 1.2.4 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -16
- package/dist/{config-DfvVZ--0.d.ts → config-CbE2t4SW.d.ts} +173 -18
- package/dist/http.d.ts +2 -1
- package/dist/http.js +636 -277
- package/dist/index.d.ts +10 -6
- package/dist/index.js +357 -105
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.js +659 -259
- 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 +357 -105
- package/dist/server/cli-http.js +1008 -327
- package/dist/server/cli-mcp.js +1031 -309
- 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-A8Q3JHwU.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
|
-
async function applyEmbedOnInsertPolicy(incoming, embedOnInsert, unified) {
|
|
362
|
-
const messages = incoming;
|
|
363
|
-
const anyMissing = incoming.some((m) => !Array.isArray(m.embedding) || m.embedding.length === 0);
|
|
364
|
-
const embedderWired = typeof unified?.embedQuery === "function";
|
|
365
|
-
if (embedOnInsert === true && anyMissing) {
|
|
366
|
-
return { messages: await embedMissingMessages(messages, unified ?? {}), warnings: [] };
|
|
367
|
-
}
|
|
368
|
-
if (embedOnInsert !== true && anyMissing && embedderWired) {
|
|
369
|
-
return {
|
|
370
|
-
messages: await embedMissingMessages(messages, unified ?? {}),
|
|
371
|
-
warnings: [AUTO_APPLIED_WARNING]
|
|
372
|
-
};
|
|
298
|
+
function resolveSearchRuntimeContext(input, runtimeContext, now = Date.now) {
|
|
299
|
+
if (runtimeContext === void 0) {
|
|
300
|
+
return void 0;
|
|
373
301
|
}
|
|
374
|
-
|
|
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;
|
|
317
|
+
}
|
|
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) {
|
|
@@ -464,7 +407,7 @@ function normalizeUnifiedMemoryMergeStrategy(value) {
|
|
|
464
407
|
return value === "rrf" ? "rrf" : "similarity";
|
|
465
408
|
}
|
|
466
409
|
function normalizeUnifiedMemoryReasoningStrategy(value, defaultStrategy = "none") {
|
|
467
|
-
if (value === "rewrite" || value === "iterative") {
|
|
410
|
+
if (value === "rewrite" || value === "iterative" || value === "union") {
|
|
468
411
|
return value;
|
|
469
412
|
}
|
|
470
413
|
return defaultStrategy;
|
|
@@ -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,8 +1130,8 @@ 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 = []) {
|
|
1161
|
-
if (reasoningStrategy === "iterative" && !deps.reasoning?.iterativePlanner) {
|
|
1133
|
+
async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = [], runtimeContext) {
|
|
1134
|
+
if ((reasoningStrategy === "iterative" || reasoningStrategy === "union") && !deps.reasoning?.iterativePlanner) {
|
|
1162
1135
|
warnings.push({
|
|
1163
1136
|
source: "memory",
|
|
1164
1137
|
code: "memory_iterative_planner_not_configured",
|
|
@@ -1174,7 +1147,8 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1174
1147
|
});
|
|
1175
1148
|
if (reasoningInfo) reasoningInfo.degraded = true;
|
|
1176
1149
|
}
|
|
1177
|
-
|
|
1150
|
+
let unionEvidence = null;
|
|
1151
|
+
if ((reasoningStrategy === "iterative" || reasoningStrategy === "union") && deps.reasoning?.iterativePlanner) {
|
|
1178
1152
|
const planner = deps.reasoning.iterativePlanner;
|
|
1179
1153
|
const result = await planner.plan({
|
|
1180
1154
|
query: input.query,
|
|
@@ -1189,7 +1163,9 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1189
1163
|
keywords2,
|
|
1190
1164
|
limit,
|
|
1191
1165
|
logger,
|
|
1192
|
-
peerPeers
|
|
1166
|
+
peerPeers,
|
|
1167
|
+
warnings,
|
|
1168
|
+
runtimeContext
|
|
1193
1169
|
);
|
|
1194
1170
|
let semanticHits = [];
|
|
1195
1171
|
if (typeof deps.embedQuery === "function") {
|
|
@@ -1203,7 +1179,9 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1203
1179
|
limit,
|
|
1204
1180
|
threshold,
|
|
1205
1181
|
logger,
|
|
1206
|
-
peerPeers
|
|
1182
|
+
peerPeers,
|
|
1183
|
+
warnings,
|
|
1184
|
+
runtimeContext
|
|
1207
1185
|
);
|
|
1208
1186
|
} catch (error) {
|
|
1209
1187
|
logger.warn?.(
|
|
@@ -1231,16 +1209,24 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1231
1209
|
reasoningInfo.degraded = true;
|
|
1232
1210
|
}
|
|
1233
1211
|
}
|
|
1234
|
-
const
|
|
1212
|
+
const evidence = result.evidence.map((candidate) => ({
|
|
1235
1213
|
type: "memory",
|
|
1236
1214
|
id: candidate.id,
|
|
1237
1215
|
content: candidate.content,
|
|
1238
1216
|
similarity: candidate.similarity,
|
|
1239
|
-
metadata: { ...candidate.metadata, reasoning:
|
|
1217
|
+
metadata: { ...candidate.metadata, reasoning: reasoningStrategy }
|
|
1240
1218
|
}));
|
|
1241
|
-
|
|
1219
|
+
if (reasoningStrategy === "iterative") {
|
|
1220
|
+
return { semantic: evidence, lexical: [] };
|
|
1221
|
+
}
|
|
1222
|
+
unionEvidence = evidence;
|
|
1242
1223
|
}
|
|
1243
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
|
+
});
|
|
1244
1230
|
warnings.push({
|
|
1245
1231
|
source: "memory",
|
|
1246
1232
|
code: "memory_lexical_search_fallback",
|
|
@@ -1248,7 +1234,16 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1248
1234
|
});
|
|
1249
1235
|
const keywords2 = deriveLexicalKeywords(input.query);
|
|
1250
1236
|
const lexical2 = filterByDateRange(
|
|
1251
|
-
await runLexicalSearchForKeywords(
|
|
1237
|
+
await runLexicalSearchForKeywords(
|
|
1238
|
+
deps,
|
|
1239
|
+
input,
|
|
1240
|
+
keywords2,
|
|
1241
|
+
limit,
|
|
1242
|
+
logger,
|
|
1243
|
+
peerPeers,
|
|
1244
|
+
warnings,
|
|
1245
|
+
runtimeContext
|
|
1246
|
+
),
|
|
1252
1247
|
input.dateFrom,
|
|
1253
1248
|
input.dateTo
|
|
1254
1249
|
);
|
|
@@ -1256,6 +1251,26 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1256
1251
|
}
|
|
1257
1252
|
let semantic = [];
|
|
1258
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
|
+
}
|
|
1259
1274
|
if (reasoningStrategy === "rewrite" && deps.reasoning?.queryRewriter) {
|
|
1260
1275
|
try {
|
|
1261
1276
|
const variants = await deps.reasoning.queryRewriter.rewrite({
|
|
@@ -1274,7 +1289,17 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1274
1289
|
);
|
|
1275
1290
|
const lists = await Promise.all(
|
|
1276
1291
|
embeddings.map(
|
|
1277
|
-
(embedding) => runSemanticSearchForEmbedding(
|
|
1292
|
+
(embedding) => runSemanticSearchForEmbedding(
|
|
1293
|
+
deps,
|
|
1294
|
+
input,
|
|
1295
|
+
embedding,
|
|
1296
|
+
limit,
|
|
1297
|
+
threshold,
|
|
1298
|
+
logger,
|
|
1299
|
+
peerPeers,
|
|
1300
|
+
warnings,
|
|
1301
|
+
runtimeContext
|
|
1302
|
+
)
|
|
1278
1303
|
)
|
|
1279
1304
|
);
|
|
1280
1305
|
semantic = mergeByMaxScore(lists);
|
|
@@ -1297,7 +1322,9 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1297
1322
|
limit,
|
|
1298
1323
|
threshold,
|
|
1299
1324
|
logger,
|
|
1300
|
-
peerPeers
|
|
1325
|
+
peerPeers,
|
|
1326
|
+
warnings,
|
|
1327
|
+
runtimeContext
|
|
1301
1328
|
);
|
|
1302
1329
|
}
|
|
1303
1330
|
let lexical = [];
|
|
@@ -1316,7 +1343,8 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1316
1343
|
limit,
|
|
1317
1344
|
botId,
|
|
1318
1345
|
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
1319
|
-
...factTypes ? { factTypes } : {}
|
|
1346
|
+
...factTypes ? { factTypes } : {},
|
|
1347
|
+
...runtimeContext ?? {}
|
|
1320
1348
|
})
|
|
1321
1349
|
)
|
|
1322
1350
|
)).flat().filter(isRawMemorySemanticResult).map((hit) => ({
|
|
@@ -1342,7 +1370,7 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1342
1370
|
let entity;
|
|
1343
1371
|
if (typeof deps.entitySearch === "function") {
|
|
1344
1372
|
entity = filterByDateRange(
|
|
1345
|
-
await runEntitySearchForQuery(deps, input, limit, logger, peerPeers),
|
|
1373
|
+
await runEntitySearchForQuery(deps, input, limit, logger, peerPeers, runtimeContext),
|
|
1346
1374
|
input.dateFrom,
|
|
1347
1375
|
input.dateTo
|
|
1348
1376
|
);
|
|
@@ -1364,6 +1392,37 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1364
1392
|
message: "RRF merge requested but entity search is not configured; falling back to semantic+lexical RRF."
|
|
1365
1393
|
});
|
|
1366
1394
|
}
|
|
1395
|
+
if (unionEvidence) {
|
|
1396
|
+
const plannerHits = unionEvidence;
|
|
1397
|
+
const base = mergeByMaxScore([
|
|
1398
|
+
filterByDateRange(semantic, input.dateFrom, input.dateTo),
|
|
1399
|
+
filterByDateRange(lexical, input.dateFrom, input.dateTo)
|
|
1400
|
+
]);
|
|
1401
|
+
const maxBase = base.length > 0 ? Math.max(...base.map((h) => h.similarity)) : 0;
|
|
1402
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1403
|
+
const merged = [];
|
|
1404
|
+
plannerHits.forEach((hit, i) => {
|
|
1405
|
+
if (seen.has(hit.id)) {
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
seen.add(hit.id);
|
|
1409
|
+
merged.push({
|
|
1410
|
+
...hit,
|
|
1411
|
+
similarity: maxBase + (plannerHits.length - i) * 1e-6
|
|
1412
|
+
});
|
|
1413
|
+
});
|
|
1414
|
+
for (const hit of base) {
|
|
1415
|
+
if (merged.length >= limit) {
|
|
1416
|
+
break;
|
|
1417
|
+
}
|
|
1418
|
+
if (seen.has(hit.id)) {
|
|
1419
|
+
continue;
|
|
1420
|
+
}
|
|
1421
|
+
seen.add(hit.id);
|
|
1422
|
+
merged.push(hit);
|
|
1423
|
+
}
|
|
1424
|
+
return { semantic: merged.slice(0, limit), lexical: [] };
|
|
1425
|
+
}
|
|
1367
1426
|
const out = {
|
|
1368
1427
|
semantic: filterByDateRange(semantic, input.dateFrom, input.dateTo),
|
|
1369
1428
|
lexical: filterByDateRange(lexical, input.dateFrom, input.dateTo)
|
|
@@ -1373,14 +1432,16 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1373
1432
|
}
|
|
1374
1433
|
return out;
|
|
1375
1434
|
}
|
|
1376
|
-
async function
|
|
1435
|
+
async function searchUnifiedMemoryWithRuntime(input, runtimeContext) {
|
|
1377
1436
|
const query = input.query.trim();
|
|
1378
1437
|
const sources = normalizeUnifiedMemorySearchSources(input.sources);
|
|
1379
1438
|
const limit = clampUnifiedMemorySearchLimit(input.limit);
|
|
1380
|
-
const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
|
|
1381
1439
|
const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
|
|
1382
1440
|
input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
|
|
1383
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;
|
|
1384
1445
|
const reasoningStrategy = resolveReasoningStrategy(input);
|
|
1385
1446
|
const warnings = [];
|
|
1386
1447
|
const peerScope = input.peerFilter && input.peerFilter.length > 0 ? await resolveScopePeer({
|
|
@@ -1400,23 +1461,46 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1400
1461
|
};
|
|
1401
1462
|
}
|
|
1402
1463
|
let memorySubs;
|
|
1464
|
+
let retrievalStatus;
|
|
1403
1465
|
const reasoningInfo = { strategy: reasoningStrategy };
|
|
1404
1466
|
if (input.dateFrom || input.dateTo) {
|
|
1405
1467
|
reasoningInfo.dateRange = { from: input.dateFrom, to: input.dateTo };
|
|
1406
1468
|
}
|
|
1407
1469
|
if (sources.includes("memory")) {
|
|
1408
|
-
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";
|
|
1409
1471
|
if (isRawMessageStorageAvailable() || hasRawProviders) {
|
|
1410
1472
|
try {
|
|
1411
1473
|
memorySubs = await runMemorySource(
|
|
1412
1474
|
input,
|
|
1413
|
-
|
|
1414
|
-
|
|
1475
|
+
candidateLimit,
|
|
1476
|
+
memoryThreshold,
|
|
1415
1477
|
warnings,
|
|
1416
1478
|
reasoningStrategy,
|
|
1417
1479
|
reasoningInfo,
|
|
1418
|
-
peerPeers
|
|
1480
|
+
peerPeers,
|
|
1481
|
+
runtimeContext
|
|
1419
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
|
+
}
|
|
1420
1504
|
} catch (error) {
|
|
1421
1505
|
logger.warn?.("[memory-store] memory source failed:", error);
|
|
1422
1506
|
warnings.push({
|
|
@@ -1439,12 +1523,13 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1439
1523
|
const hits = await deps.searchInsights({
|
|
1440
1524
|
userId: input.userId,
|
|
1441
1525
|
query: input.query,
|
|
1442
|
-
limit,
|
|
1526
|
+
limit: candidateLimit,
|
|
1443
1527
|
threshold,
|
|
1444
1528
|
botIds: input.botIds,
|
|
1445
1529
|
includeArchived: input.includeArchivedInsights,
|
|
1446
1530
|
authToken: input.authToken,
|
|
1447
|
-
...peerPeers.length > 0 ? { peers: peerPeers } : {}
|
|
1531
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
1532
|
+
...runtimeContext ?? {}
|
|
1448
1533
|
});
|
|
1449
1534
|
for (const hit of hits) {
|
|
1450
1535
|
insightHits.push({
|
|
@@ -1471,12 +1556,13 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1471
1556
|
userId: input.userId,
|
|
1472
1557
|
query: input.query,
|
|
1473
1558
|
options: {
|
|
1474
|
-
limit,
|
|
1559
|
+
limit: candidateLimit,
|
|
1475
1560
|
threshold,
|
|
1476
1561
|
documentIds: input.documentIds
|
|
1477
1562
|
},
|
|
1478
1563
|
authToken: input.authToken,
|
|
1479
|
-
...peerPeers.length > 0 ? { peers: peerPeers } : {}
|
|
1564
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
1565
|
+
...runtimeContext ?? {}
|
|
1480
1566
|
});
|
|
1481
1567
|
for (const hit of hits) {
|
|
1482
1568
|
knowledgeHits.push(toKnowledgeResult(hit));
|
|
@@ -1494,10 +1580,14 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1494
1580
|
memorySubs,
|
|
1495
1581
|
insightHits,
|
|
1496
1582
|
knowledgeHits,
|
|
1497
|
-
limit,
|
|
1583
|
+
limit: candidateLimit,
|
|
1498
1584
|
strategy: mergeStrategy
|
|
1499
1585
|
});
|
|
1500
|
-
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);
|
|
1501
1591
|
const output = {
|
|
1502
1592
|
query: input.query,
|
|
1503
1593
|
sources,
|
|
@@ -1505,28 +1595,51 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1505
1595
|
count: ranked.length,
|
|
1506
1596
|
warnings
|
|
1507
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
|
+
}
|
|
1508
1631
|
if (reasoningInfo && (reasoningInfo.strategy !== "none" || reasoningInfo.dateRange !== void 0)) {
|
|
1509
1632
|
output.reasoning = reasoningInfo;
|
|
1510
1633
|
}
|
|
1511
1634
|
return output;
|
|
1512
1635
|
}
|
|
1636
|
+
async function searchUnifiedMemory(input) {
|
|
1637
|
+
return searchUnifiedMemoryWithRuntime(input);
|
|
1638
|
+
}
|
|
1513
1639
|
async function searchRawMemorySemantically(input) {
|
|
1514
|
-
|
|
1515
|
-
const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
|
|
1516
|
-
const reasoningStrategy = resolveReasoningStrategy(input);
|
|
1517
|
-
const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
|
|
1518
|
-
input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
|
|
1519
|
-
);
|
|
1520
|
-
const sub = await runMemorySource(input, limit, threshold, [], reasoningStrategy);
|
|
1521
|
-
return mergeAcrossSources({
|
|
1522
|
-
memorySubs: sub,
|
|
1523
|
-
insightHits: [],
|
|
1524
|
-
knowledgeHits: [],
|
|
1525
|
-
limit,
|
|
1526
|
-
strategy: mergeStrategy
|
|
1527
|
-
});
|
|
1640
|
+
return (await searchUnifiedMemory({ ...input, sources: ["memory"] })).results;
|
|
1528
1641
|
}
|
|
1529
|
-
async function
|
|
1642
|
+
async function searchWithResolvedRuntime(input, runtimeContext) {
|
|
1530
1643
|
const query = input.query.trim();
|
|
1531
1644
|
const wantsSynthesis = typeof input.synthesize === "boolean" ? input.synthesize : typeof input.synthesize === "object" && input.synthesize !== null;
|
|
1532
1645
|
const synthesisSchema = typeof input.synthesize === "object" && input.synthesize !== null ? input.synthesize.responseSchema ?? input.responseSchema : input.responseSchema;
|
|
@@ -1558,19 +1671,24 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1558
1671
|
let unifiedHits = [];
|
|
1559
1672
|
let sources = input.sources ? [...input.sources] : [];
|
|
1560
1673
|
let unifiedReasoning;
|
|
1674
|
+
let retrievalDiagnostics;
|
|
1561
1675
|
if (wantsRawTier || wantsInsightTier || wantsKnowledgeTier) {
|
|
1562
1676
|
const inferredSources = sources.length > 0 ? sources : [
|
|
1563
1677
|
...wantsRawTier ? ["memory"] : [],
|
|
1564
1678
|
...wantsInsightTier ? ["insights"] : [],
|
|
1565
1679
|
...wantsKnowledgeTier ? ["knowledge"] : []
|
|
1566
1680
|
];
|
|
1567
|
-
const unified = await
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1681
|
+
const unified = await searchUnifiedMemoryWithRuntime(
|
|
1682
|
+
{
|
|
1683
|
+
...searchInputToUnified(input),
|
|
1684
|
+
sources: inferredSources.length > 0 ? inferredSources : void 0
|
|
1685
|
+
},
|
|
1686
|
+
runtimeContext
|
|
1687
|
+
);
|
|
1571
1688
|
unifiedHits = unified.results;
|
|
1572
1689
|
sources = unified.sources;
|
|
1573
1690
|
unifiedReasoning = unified.reasoning;
|
|
1691
|
+
retrievalDiagnostics = unified.retrievalDiagnostics;
|
|
1574
1692
|
warnings.push(...unified.warnings);
|
|
1575
1693
|
}
|
|
1576
1694
|
let summaryHits = [];
|
|
@@ -1581,7 +1699,8 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1581
1699
|
limit,
|
|
1582
1700
|
threshold,
|
|
1583
1701
|
logger,
|
|
1584
|
-
peerScope.peers
|
|
1702
|
+
peerScope.peers,
|
|
1703
|
+
runtimeContext
|
|
1585
1704
|
);
|
|
1586
1705
|
warnings.push(...summaryBucket.warnings);
|
|
1587
1706
|
summaryHits = summaryBucket.hits;
|
|
@@ -1606,7 +1725,8 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1606
1725
|
source: mapHitToEvidenceSource(hit),
|
|
1607
1726
|
snippet: hit.content,
|
|
1608
1727
|
score: hit.similarity,
|
|
1609
|
-
timestamp: getCandidateTimestamp(hit.metadata)
|
|
1728
|
+
timestamp: getCandidateTimestamp(hit.metadata),
|
|
1729
|
+
metadata: hit.metadata
|
|
1610
1730
|
}));
|
|
1611
1731
|
const base = {
|
|
1612
1732
|
query,
|
|
@@ -1619,12 +1739,15 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1619
1739
|
if (unifiedReasoning) {
|
|
1620
1740
|
base.reasoning = unifiedReasoning;
|
|
1621
1741
|
}
|
|
1742
|
+
if (retrievalDiagnostics) {
|
|
1743
|
+
base.retrievalDiagnostics = retrievalDiagnostics;
|
|
1744
|
+
}
|
|
1622
1745
|
if (!wantsSynthesis) {
|
|
1623
1746
|
return base;
|
|
1624
1747
|
}
|
|
1625
1748
|
const { answer, warnings: synthWarnings } = await synthesizeAnswer({
|
|
1626
1749
|
query,
|
|
1627
|
-
|
|
1750
|
+
evidence,
|
|
1628
1751
|
responseSchema: synthesisSchema,
|
|
1629
1752
|
deps,
|
|
1630
1753
|
logger
|
|
@@ -1633,6 +1756,10 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1633
1756
|
base.warnings = [...base.warnings, ...synthWarnings];
|
|
1634
1757
|
return base;
|
|
1635
1758
|
}
|
|
1759
|
+
async function search(input, runtimeContext) {
|
|
1760
|
+
const resolvedRuntime = resolveSearchRuntimeContext(input, runtimeContext);
|
|
1761
|
+
return searchWithResolvedRuntime(input, resolvedRuntime);
|
|
1762
|
+
}
|
|
1636
1763
|
return {
|
|
1637
1764
|
search,
|
|
1638
1765
|
searchUnifiedMemory,
|
|
@@ -1648,6 +1775,7 @@ function mergeAcrossSources(input) {
|
|
|
1648
1775
|
for (const hit of [
|
|
1649
1776
|
...memorySubs.semantic,
|
|
1650
1777
|
...memorySubs.lexical,
|
|
1778
|
+
...memorySubs.hybrid ?? [],
|
|
1651
1779
|
...input.insightHits,
|
|
1652
1780
|
...input.knowledgeHits
|
|
1653
1781
|
]) {
|
|
@@ -1663,6 +1791,7 @@ function mergeAcrossSources(input) {
|
|
|
1663
1791
|
const all = [
|
|
1664
1792
|
...memorySubs.semantic,
|
|
1665
1793
|
...memorySubs.lexical,
|
|
1794
|
+
...memorySubs.hybrid ?? [],
|
|
1666
1795
|
...memorySubs.entity ?? [],
|
|
1667
1796
|
...input.insightHits,
|
|
1668
1797
|
...input.knowledgeHits
|
|
@@ -1674,6 +1803,9 @@ function buildChannelLists(memorySubs, insightHits, knowledgeHits) {
|
|
|
1674
1803
|
const lists = [];
|
|
1675
1804
|
if (memorySubs.semantic.length > 0) lists.push({ name: "memory-semantic", hits: memorySubs.semantic });
|
|
1676
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
|
+
}
|
|
1677
1809
|
if (memorySubs.entity && memorySubs.entity.length > 0) {
|
|
1678
1810
|
lists.push({ name: "memory-entity", hits: memorySubs.entity });
|
|
1679
1811
|
}
|
|
@@ -2138,8 +2270,250 @@ function createVsaRecall(storage) {
|
|
|
2138
2270
|
};
|
|
2139
2271
|
}
|
|
2140
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
|
+
|
|
2141
2516
|
// src/mcp.ts
|
|
2142
|
-
init_chroma_memory_index();
|
|
2143
2517
|
init_raw_message_store();
|
|
2144
2518
|
init_sqlite_raw_message_store();
|
|
2145
2519
|
var DEFAULT_NAME = "@melandlabs/memory-store";
|
|
@@ -2152,13 +2526,37 @@ async function startMcpServer(options = {}) {
|
|
|
2152
2526
|
const rawStore = createRawMessageStore({
|
|
2153
2527
|
env: options.env
|
|
2154
2528
|
});
|
|
2155
|
-
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
|
+
});
|
|
2156
2543
|
const vsaDbPath = resolveSQLiteRawMessageDbPath(options.dbPath);
|
|
2157
2544
|
const vsaStorage = await getSQLiteVsaStore({ dbPath: vsaDbPath });
|
|
2158
2545
|
const vsa = createVsaRecall(vsaStorage);
|
|
2159
|
-
server.tool("memory.health", "Returns the health status of the memory store.", async () =>
|
|
2160
|
-
|
|
2161
|
-
|
|
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
|
+
});
|
|
2162
2560
|
const searchSchema = {
|
|
2163
2561
|
userId: z.string(),
|
|
2164
2562
|
query: z.string().min(1),
|
|
@@ -2166,8 +2564,8 @@ async function startMcpServer(options = {}) {
|
|
|
2166
2564
|
tiers: z.array(z.enum(["summary", "raw", "insight", "knowledge"])).optional().describe(
|
|
2167
2565
|
"Per-tier evidence scope for synthesis. Default: ['summary','raw','insight','knowledge']. Ignored when `synthesize` is falsy."
|
|
2168
2566
|
),
|
|
2169
|
-
reasoningStrategy: z.enum(["none", "rewrite", "iterative"]).optional().describe(
|
|
2170
|
-
"LLM reasoning strategy. `rewrite` rephrases the query before embedding; `iterative` runs a planner that searches, notes evidence, and searches again. Requires `unified.reasoning.{queryRewriter,iterativePlanner}` to be wired into the server."
|
|
2567
|
+
reasoningStrategy: z.enum(["none", "rewrite", "iterative", "union"]).optional().describe(
|
|
2568
|
+
"LLM reasoning strategy. `rewrite` rephrases the query before embedding; `iterative` runs a planner that searches, notes evidence, and searches again; `union` merges the planner's evidence with the baseline top-k hits (evidence first, deduped, capped at limit). Requires `unified.reasoning.{queryRewriter,iterativePlanner}` to be wired into the server."
|
|
2171
2569
|
),
|
|
2172
2570
|
synthesize: z.union([z.boolean(), z.object({ responseSchema: z.record(z.string(), z.unknown()).optional() })]).optional().describe(
|
|
2173
2571
|
"Opt-in LLM synthesis. `true` returns `{ answer, evidence, results, warnings }`. `synthesize.responseSchema` extracts `answer` from a JSON payload."
|
|
@@ -2176,7 +2574,11 @@ async function startMcpServer(options = {}) {
|
|
|
2176
2574
|
// coerces it into `synthesize.responseSchema` when present.
|
|
2177
2575
|
responseSchema: z.record(z.string(), z.unknown()).optional(),
|
|
2178
2576
|
limit: z.number().int().min(1).max(50).default(10),
|
|
2179
|
-
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(),
|
|
2180
2582
|
botIds: z.array(z.string()).optional(),
|
|
2181
2583
|
documentIds: z.array(z.string()).optional(),
|
|
2182
2584
|
includeArchivedInsights: z.boolean().default(false),
|
|
@@ -2240,11 +2642,13 @@ async function startMcpServer(options = {}) {
|
|
|
2240
2642
|
sources: a.sources,
|
|
2241
2643
|
limit: a.limit,
|
|
2242
2644
|
threshold: a.threshold,
|
|
2645
|
+
mergeStrategy: a.mergeStrategy,
|
|
2646
|
+
includeRetrievalDiagnostics: a.includeRetrievalDiagnostics,
|
|
2243
2647
|
botIds: a.botIds,
|
|
2244
2648
|
documentIds: a.documentIds,
|
|
2245
2649
|
authToken: a.authToken,
|
|
2246
2650
|
includeArchivedInsights: a.includeArchivedInsights,
|
|
2247
|
-
reasoningStrategy: a.reasoningStrategy === "rewrite" || a.reasoningStrategy === "iterative" ? a.reasoningStrategy : void 0,
|
|
2651
|
+
reasoningStrategy: a.reasoningStrategy === "rewrite" || a.reasoningStrategy === "iterative" || a.reasoningStrategy === "union" ? a.reasoningStrategy : void 0,
|
|
2248
2652
|
...a.tiers ? { tiers: a.tiers } : {},
|
|
2249
2653
|
...wantsSynthesis ? {
|
|
2250
2654
|
synthesize: {
|
|
@@ -2270,37 +2674,24 @@ async function startMcpServer(options = {}) {
|
|
|
2270
2674
|
const a = args;
|
|
2271
2675
|
const manager2 = await rawStore.getManager();
|
|
2272
2676
|
const incoming = [a.message].map((m) => ({ ...m, userId: m.userId ?? a.userId }));
|
|
2273
|
-
const
|
|
2274
|
-
|
|
2275
|
-
a.
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
messages
|
|
2283
|
-
});
|
|
2284
|
-
} else if (typeof manager2.storeMessages === "function") {
|
|
2285
|
-
const ids = await manager2.storeMessages(messages);
|
|
2286
|
-
result = { inserted: ids.length, ids };
|
|
2287
|
-
} else {
|
|
2288
|
-
throw new Error("active raw-message manager exposes neither upsertRawMessages nor storeMessages");
|
|
2289
|
-
}
|
|
2290
|
-
try {
|
|
2291
|
-
await upsertRawMessagesToChroma(messages);
|
|
2292
|
-
} catch (error) {
|
|
2293
|
-
console.warn("[memory-store/mcp] chroma upsert failed:", error);
|
|
2294
|
-
}
|
|
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 };
|
|
2295
2686
|
return {
|
|
2296
2687
|
content: [
|
|
2297
2688
|
{
|
|
2298
2689
|
type: "text",
|
|
2299
2690
|
text: JSON.stringify({
|
|
2300
2691
|
ok: true,
|
|
2301
|
-
count:
|
|
2692
|
+
count: persisted.count,
|
|
2302
2693
|
result,
|
|
2303
|
-
...warnings.length > 0 ? { warnings } : {}
|
|
2694
|
+
...persisted.warnings.length > 0 ? { warnings: persisted.warnings } : {}
|
|
2304
2695
|
})
|
|
2305
2696
|
}
|
|
2306
2697
|
]
|
|
@@ -2640,7 +3031,17 @@ async function startMcpServer(options = {}) {
|
|
|
2640
3031
|
);
|
|
2641
3032
|
const okfMcpSpecifier = "@melandlabs/okf/mcp";
|
|
2642
3033
|
const { registerOkfTools } = await import(okfMcpSpecifier);
|
|
2643
|
-
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
|
+
});
|
|
2644
3045
|
const transport = new StdioServerTransport();
|
|
2645
3046
|
await server.connect(transport);
|
|
2646
3047
|
return server;
|
|
@@ -2985,6 +3386,121 @@ function createUserVoiceRewriter(options) {
|
|
|
2985
3386
|
};
|
|
2986
3387
|
}
|
|
2987
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
|
+
|
|
2988
3504
|
// src/server/cli-shared.ts
|
|
2989
3505
|
init_raw_message_store();
|
|
2990
3506
|
|
|
@@ -2999,7 +3515,7 @@ function isInsightSQLiteVecEnabled(_env) {
|
|
|
2999
3515
|
}
|
|
3000
3516
|
async function getInsightSQLiteVecStore(_env) {
|
|
3001
3517
|
const dbPath = resolveSQLiteRawMessageDbPath();
|
|
3002
|
-
const { getSQLiteVecStore } = await import('@melandlabs/rag/sqlite-vec-store');
|
|
3518
|
+
const { getSQLiteVecStore } = await import('@melandlabs/ai-rag/sqlite-vec-store');
|
|
3003
3519
|
return await getSQLiteVecStore(dbPath, void 0, {
|
|
3004
3520
|
collectionName: process.env.SQLITE_VEC_INSIGHTS_COLLECTION || "opencontext_insights"
|
|
3005
3521
|
});
|
|
@@ -3023,17 +3539,19 @@ async function searchInsightsWithSQLiteVec(input, env) {
|
|
|
3023
3539
|
// src/server/cli-shared.ts
|
|
3024
3540
|
async function loadAiRag() {
|
|
3025
3541
|
try {
|
|
3026
|
-
const [local, chroma] = await Promise.all([
|
|
3542
|
+
const [local, reranker, chroma] = await Promise.all([
|
|
3027
3543
|
import('@melandlabs/ai-rag/local-transformers-embedding-provider'),
|
|
3544
|
+
import('@melandlabs/ai-rag/local-transformers-reranker'),
|
|
3028
3545
|
import('@melandlabs/ai-rag/chroma-store')
|
|
3029
3546
|
]);
|
|
3030
3547
|
return {
|
|
3031
3548
|
LocalTransformersEmbeddingProvider: local.LocalTransformersEmbeddingProvider,
|
|
3549
|
+
LocalTransformersReranker: reranker.LocalTransformersReranker,
|
|
3032
3550
|
ChromaVectorStore: chroma.ChromaVectorStore
|
|
3033
3551
|
};
|
|
3034
3552
|
} catch (error) {
|
|
3035
3553
|
throw new Error(
|
|
3036
|
-
|
|
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})`
|
|
3037
3555
|
);
|
|
3038
3556
|
}
|
|
3039
3557
|
}
|
|
@@ -3042,8 +3560,21 @@ function parseUnifiedArgs(argv) {
|
|
|
3042
3560
|
const args = {
|
|
3043
3561
|
embeddingProvider: env.EMBEDDING_PROVIDER ?? "none",
|
|
3044
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,
|
|
3045
3569
|
chromaUrl: env.CHROMA_URL,
|
|
3046
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,
|
|
3047
3578
|
insightsBackend: env.INSIGHTS_BACKEND ?? "none",
|
|
3048
3579
|
insightsCollection: env.INSIGHTS_COLLECTION ?? "opencontext_insights",
|
|
3049
3580
|
knowledgeBackend: env.KNOWLEDGE_BACKEND ?? "none",
|
|
@@ -3068,12 +3599,51 @@ function parseUnifiedArgs(argv) {
|
|
|
3068
3599
|
case "--embedding-model":
|
|
3069
3600
|
args.embeddingModel = takeValue();
|
|
3070
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;
|
|
3071
3620
|
case "--chroma-url":
|
|
3072
3621
|
args.chromaUrl = takeValue();
|
|
3073
3622
|
break;
|
|
3074
3623
|
case "--memory-backend":
|
|
3075
3624
|
args.memoryBackend = takeValue();
|
|
3076
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;
|
|
3077
3647
|
case "--insights-backend":
|
|
3078
3648
|
args.insightsBackend = takeValue();
|
|
3079
3649
|
break;
|
|
@@ -3103,31 +3673,60 @@ function parseUnifiedArgs(argv) {
|
|
|
3103
3673
|
break;
|
|
3104
3674
|
}
|
|
3105
3675
|
}
|
|
3106
|
-
|
|
3107
|
-
["--embedding-provider", args.embeddingProvider],
|
|
3108
|
-
["--memory-backend", args.memoryBackend],
|
|
3109
|
-
["--insights-backend", args.insightsBackend],
|
|
3110
|
-
["--knowledge-backend", args.knowledgeBackend]
|
|
3111
|
-
]) {
|
|
3112
|
-
const allowed = ["local", "openrouter", "none", "chroma", "sqlite-vec"];
|
|
3676
|
+
const validate = (name, value, allowed) => {
|
|
3113
3677
|
if (!allowed.includes(value)) {
|
|
3114
3678
|
throw new Error(`[memory-store/cli] ${name} must be one of: ${allowed.join(", ")} (got "${value}")`);
|
|
3115
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
|
+
}
|
|
3116
3696
|
}
|
|
3117
3697
|
return args;
|
|
3118
3698
|
}
|
|
3119
3699
|
async function buildUnified(args) {
|
|
3120
3700
|
const unified = {};
|
|
3121
3701
|
const log = (msg) => console.warn(`[memory-store/cli] ${msg}`);
|
|
3122
|
-
const wantsAiRag = args.embeddingProvider === "local" || args.
|
|
3702
|
+
const wantsAiRag = args.embeddingProvider === "local" || args.rerankerProvider === "local" || args.insightsBackend === "chroma" || args.knowledgeBackend === "chroma";
|
|
3123
3703
|
const aiRag = wantsAiRag ? await loadAiRag() : null;
|
|
3124
3704
|
if (args.embeddingProvider === "local") {
|
|
3125
3705
|
try {
|
|
3126
3706
|
if (!aiRag) {
|
|
3127
3707
|
throw new Error("ai-rag modules not loaded");
|
|
3128
3708
|
}
|
|
3129
|
-
const provider = new aiRag.LocalTransformersEmbeddingProvider({
|
|
3130
|
-
|
|
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
|
+
};
|
|
3131
3730
|
log(
|
|
3132
3731
|
`embedQuery wired via LocalTransformersEmbeddingProvider (model=${args.embeddingModel ?? "Xenova/all-MiniLM-L6-v2"})`
|
|
3133
3732
|
);
|
|
@@ -3143,6 +3742,7 @@ async function buildUnified(args) {
|
|
|
3143
3742
|
}
|
|
3144
3743
|
const model = args.embeddingModel ?? "text-embedding-3-small";
|
|
3145
3744
|
const baseURL = "https://openrouter.ai/api/v1";
|
|
3745
|
+
unified.embeddingInfo = { provider: "openrouter", model };
|
|
3146
3746
|
unified.embedQuery = async ({ query }) => {
|
|
3147
3747
|
const res = await fetch(`${baseURL}/embeddings`, {
|
|
3148
3748
|
method: "POST",
|
|
@@ -3160,49 +3760,150 @@ async function buildUnified(args) {
|
|
|
3160
3760
|
const body = await res.json();
|
|
3161
3761
|
const embedding = body.data?.[0]?.embedding;
|
|
3162
3762
|
if (!embedding) throw new Error("openrouter embeddings response missing data[0].embedding");
|
|
3763
|
+
if (unified.embeddingInfo) unified.embeddingInfo.dimensions = embedding.length;
|
|
3163
3764
|
return embedding;
|
|
3164
3765
|
};
|
|
3165
3766
|
log(`embedQuery wired via openrouter (model=${model})`);
|
|
3166
3767
|
}
|
|
3167
|
-
if (args.
|
|
3168
|
-
|
|
3169
|
-
const
|
|
3170
|
-
|
|
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") {
|
|
3171
3819
|
throw new Error(
|
|
3172
|
-
|
|
3820
|
+
`${args.memoryBackend} child retrieval requires the SQLite raw-message catalog; the Postgres manager remains on the legacy parent fallback in this release`
|
|
3173
3821
|
);
|
|
3174
3822
|
}
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
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
|
|
3182
3853
|
});
|
|
3183
|
-
return rows.map((r) => ({
|
|
3184
|
-
id: r.id,
|
|
3185
|
-
content: r.content,
|
|
3186
|
-
similarity: r.similarity,
|
|
3187
|
-
metadata: r.metadata ?? {}
|
|
3188
|
-
}));
|
|
3189
|
-
};
|
|
3190
|
-
log("memory backend wired via sqlite-vec (uses the manager's searchMessagesSemantically)");
|
|
3191
|
-
} else if (args.memoryBackend === "chroma") {
|
|
3192
|
-
if (!args.chromaUrl)
|
|
3193
|
-
throw new Error("--memory-backend=chroma requires --chroma-url <url> (or CHROMA_URL env)");
|
|
3194
|
-
if (!aiRag) {
|
|
3195
|
-
throw new Error("ai-rag modules not loaded");
|
|
3196
3854
|
}
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
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
|
+
};
|
|
3204
3905
|
};
|
|
3205
|
-
log(
|
|
3906
|
+
log(`memory backend wired via ${args.memoryBackend} with SQLite child catalog and lexical search`);
|
|
3206
3907
|
}
|
|
3207
3908
|
if (args.insightsBackend === "sqlite-vec" && isInsightSQLiteVecEnabled()) {
|
|
3208
3909
|
if (!unified.embedQuery) {
|
|
@@ -3329,20 +4030,41 @@ function printUnifiedHelp() {
|
|
|
3329
4030
|
--embedding-model <name> Model name
|
|
3330
4031
|
(env: EMBEDDING_MODEL; local \u2192 Xenova/all-MiniLM-L6-v2,
|
|
3331
4032
|
openrouter \u2192 text-embedding-3-small)
|
|
4033
|
+
--embedding-cache-dir <path> Local embedding cache (env: LOCAL_EMBEDDING_CACHE_DIR)
|
|
3332
4034
|
Note: "local" dynamically imports @melandlabs/ai-rag (a peer install).
|
|
3333
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
|
+
|
|
3334
4048
|
Cross-source search (wires unified.searchKnowledge / searchInsights / searchRawMessagesAnn):
|
|
3335
4049
|
--chroma-url <url> Chroma server URL
|
|
3336
4050
|
(env: CHROMA_URL; required when any *-backend=chroma)
|
|
3337
|
-
--memory-backend <name> sqlite-vec | chroma | none
|
|
4051
|
+
--memory-backend <name> sqlite-vec | chroma | lancedb | milvus | none
|
|
3338
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)
|
|
3339
4060
|
--insights-backend <name> sqlite-vec | chroma | none
|
|
3340
4061
|
(env: INSIGHTS_BACKEND, default: none)
|
|
3341
4062
|
--insights-collection <name> Chroma collection (default: opencontext_insights)
|
|
3342
4063
|
--knowledge-backend <name> chroma | none
|
|
3343
4064
|
(env: KNOWLEDGE_BACKEND, default: none)
|
|
3344
4065
|
--knowledge-collection <name> Chroma collection (default: opencontext_knowledge)
|
|
3345
|
-
|
|
4066
|
+
Notes: memory Chroma uses @melandlabs/rag; insight/knowledge Chroma use
|
|
4067
|
+
@melandlabs/ai-rag. LanceDB and Milvus require their optional peer dependencies.
|
|
3346
4068
|
|
|
3347
4069
|
Reasoning (wires unified.reasoning.{queryRewriter, iterativePlanner}):
|
|
3348
4070
|
--reasoning Enable the LLM reasoning layer so /v1/search and
|