@melandlabs/memory-store 1.2.5 → 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-Dw4aXj2a.d.ts → config-CbE2t4SW.d.ts} +172 -17
- package/dist/http.d.ts +2 -1
- package/dist/http.js +594 -270
- package/dist/index.d.ts +10 -6
- package/dist/index.js +316 -99
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.js +615 -250
- package/dist/search/derive.d.ts +2 -1
- package/dist/search/distill.d.ts +2 -1
- package/dist/search/unified-search.d.ts +3 -2
- package/dist/search/unified-search.js +316 -99
- package/dist/server/cli-http.js +966 -320
- package/dist/server/cli-mcp.js +987 -300
- package/dist/server/cli-shared.d.ts +20 -7
- package/dist/server/cli-shared.js +370 -48
- package/dist/storage/postgres-raw-message-factory.d.ts +2 -1
- package/dist/storage/raw-message-store.d.ts +20 -2
- package/dist/storage/sqlite-raw-message-store.d.ts +8 -1
- package/dist/storage/sqlite-vector-index.d.ts +2 -1
- package/dist/storage/sqlite-vector-index.js +1 -1
- package/dist/{unified-search-DfE8ek7W.d.ts → unified-search-2YQ6EGuQ.d.ts} +17 -6
- package/package.json +16 -12
package/dist/server/cli-http.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';
|
|
@@ -12,6 +12,8 @@ import { applyReflectedConsolidationPlan } from '@melandlabs/memory-consolidatio
|
|
|
12
12
|
import { isDerivedKind } from '@melandlabs/contracts/derived-fact';
|
|
13
13
|
import { isEntityKind } from '@melandlabs/contracts/entity-edge';
|
|
14
14
|
import { superpose, bind, unbind, cleanup, cosineSimilarity } from '@melandlabs/vsa';
|
|
15
|
+
import { createHash } from 'crypto';
|
|
16
|
+
import { ChromaVectorStore } from '@melandlabs/rag/chroma-vector-store';
|
|
15
17
|
|
|
16
18
|
var __defProp = Object.defineProperty;
|
|
17
19
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -22,27 +24,6 @@ var __export = (target, all) => {
|
|
|
22
24
|
for (var name in all)
|
|
23
25
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
24
26
|
};
|
|
25
|
-
function normalizeTimestampToMs(value) {
|
|
26
|
-
if (!Number.isFinite(value)) {
|
|
27
|
-
return 0;
|
|
28
|
-
}
|
|
29
|
-
if (value < 1e11) {
|
|
30
|
-
return Math.floor(value * 1e3);
|
|
31
|
-
}
|
|
32
|
-
return Math.floor(value);
|
|
33
|
-
}
|
|
34
|
-
function rawMessageToMemoryRecord(message) {
|
|
35
|
-
return {
|
|
36
|
-
id: message.messageId,
|
|
37
|
-
userId: message.userId,
|
|
38
|
-
timestamp: normalizeTimestampToMs(message.timestamp),
|
|
39
|
-
text: message.archivedAt ? void 0 : message.content,
|
|
40
|
-
mediaRefs: message.attachments?.map((item) => item.url).filter(Boolean),
|
|
41
|
-
embedding: message.embedding,
|
|
42
|
-
embeddingModel: message.embeddingModel,
|
|
43
|
-
embeddingContentHash: message.embeddingContentHash
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
27
|
var init_contracts = __esm({
|
|
47
28
|
"src/contracts.ts"() {
|
|
48
29
|
}
|
|
@@ -63,66 +44,14 @@ function getRawMessagesCollectionName() {
|
|
|
63
44
|
return process.env.CHROMA_RAW_MESSAGES_COLLECTION || process.env.CHROMA_MEMORY_COLLECTION || DEFAULT_RAW_MESSAGES_COLLECTION;
|
|
64
45
|
}
|
|
65
46
|
function getChromaStore(collectionName) {
|
|
66
|
-
return new ChromaVectorStore({
|
|
47
|
+
return new ChromaVectorStore$1({
|
|
67
48
|
url: process.env.CHROMA_URL,
|
|
68
49
|
collectionName
|
|
69
50
|
});
|
|
70
51
|
}
|
|
71
|
-
function normalizeTimestampToMs2(value) {
|
|
72
|
-
if (!Number.isFinite(value)) {
|
|
73
|
-
return 0;
|
|
74
|
-
}
|
|
75
|
-
if (value < 1e11) {
|
|
76
|
-
return Math.floor(value * 1e3);
|
|
77
|
-
}
|
|
78
|
-
return Math.floor(value);
|
|
79
|
-
}
|
|
80
52
|
function toBooleanValue(value) {
|
|
81
53
|
return value === true || value === "true" || value === 1 || value === "1";
|
|
82
54
|
}
|
|
83
|
-
function rawMessageToChunk(message) {
|
|
84
|
-
if (!message.embedding || message.embedding.length === 0) {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
const record = rawMessageToMemoryRecord(message);
|
|
88
|
-
const document = buildMemoryRecordEmbeddingDocument(record);
|
|
89
|
-
if (!document.content) {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
return {
|
|
93
|
-
id: message.messageId,
|
|
94
|
-
documentId: message.messageId,
|
|
95
|
-
content: document.content,
|
|
96
|
-
embedding: message.embedding,
|
|
97
|
-
metadata: {
|
|
98
|
-
sourceType: "raw_message",
|
|
99
|
-
userId: message.userId,
|
|
100
|
-
platform: message.platform,
|
|
101
|
-
botId: message.botId,
|
|
102
|
-
channel: message.channel,
|
|
103
|
-
person: message.person,
|
|
104
|
-
timestamp: normalizeTimestampToMs2(message.timestamp),
|
|
105
|
-
memoryStage: message.memoryStage,
|
|
106
|
-
embeddingModel: message.embeddingModel,
|
|
107
|
-
embeddingDimensions: message.embeddingDimensions,
|
|
108
|
-
contentHash: message.embeddingContentHash,
|
|
109
|
-
archived: Boolean(message.archivedAt),
|
|
110
|
-
role: typeof message.metadata?.role === "string" ? message.metadata.role : void 0
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
async function upsertRawMessagesToChroma(messages) {
|
|
115
|
-
if (!isRawMessageChromaEnabled() || messages.length === 0) {
|
|
116
|
-
return 0;
|
|
117
|
-
}
|
|
118
|
-
const chunks = messages.map(rawMessageToChunk).filter((chunk) => chunk !== null);
|
|
119
|
-
if (chunks.length === 0) {
|
|
120
|
-
return 0;
|
|
121
|
-
}
|
|
122
|
-
const store = getChromaStore(getRawMessagesCollectionName());
|
|
123
|
-
await store.addChunks(chunks);
|
|
124
|
-
return chunks.length;
|
|
125
|
-
}
|
|
126
55
|
async function searchRawMessagesWithChroma(input) {
|
|
127
56
|
if (!isRawMessageChromaEnabled() || input.queryEmbedding.length === 0) {
|
|
128
57
|
return [];
|
|
@@ -324,59 +253,73 @@ var init_raw_message_store = __esm({
|
|
|
324
253
|
}
|
|
325
254
|
});
|
|
326
255
|
|
|
327
|
-
// src/
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
256
|
+
// src/search/unified-search.ts
|
|
257
|
+
init_chroma_memory_index();
|
|
258
|
+
init_raw_message_store();
|
|
259
|
+
|
|
260
|
+
// src/search/applicability.ts
|
|
261
|
+
var APPLICABILITY_SCOPES = /* @__PURE__ */ new Set([
|
|
262
|
+
"global",
|
|
263
|
+
"task",
|
|
264
|
+
"conversation",
|
|
265
|
+
"channel",
|
|
266
|
+
"project",
|
|
267
|
+
"custom"
|
|
268
|
+
]);
|
|
269
|
+
function isRecord(value) {
|
|
270
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
271
|
+
}
|
|
272
|
+
function invalidRuntimeContext(message) {
|
|
273
|
+
throw new TypeError(`memory-store search runtime context: ${message}`);
|
|
274
|
+
}
|
|
275
|
+
function validateApplicabilityContext(value, index) {
|
|
276
|
+
const path = `applicabilityContexts[${index}]`;
|
|
277
|
+
if (!isRecord(value)) {
|
|
278
|
+
invalidRuntimeContext(`${path} must be an object`);
|
|
331
279
|
}
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
280
|
+
const scope = value.scope;
|
|
281
|
+
if (typeof scope !== "string" || !APPLICABILITY_SCOPES.has(scope)) {
|
|
282
|
+
invalidRuntimeContext(`${path}.scope is not supported`);
|
|
283
|
+
}
|
|
284
|
+
if (value.key !== void 0 && typeof value.key !== "string") {
|
|
285
|
+
invalidRuntimeContext(`${path}.key must be a string when provided`);
|
|
286
|
+
}
|
|
287
|
+
if (scope !== "global" && (typeof value.key !== "string" || value.key.trim().length === 0)) {
|
|
288
|
+
invalidRuntimeContext(`${path}.key must be non-empty for scope "${scope}"`);
|
|
289
|
+
}
|
|
290
|
+
for (const field of ["validFrom", "validUntil"]) {
|
|
291
|
+
const boundary = value[field];
|
|
292
|
+
if (boundary !== void 0 && (typeof boundary !== "number" || !Number.isFinite(boundary))) {
|
|
293
|
+
invalidRuntimeContext(`${path}.${field} must be a finite epoch-millisecond number`);
|
|
341
294
|
}
|
|
342
|
-
const vector = await deps.embedQuery({
|
|
343
|
-
userId: message.userId,
|
|
344
|
-
query: message.content
|
|
345
|
-
});
|
|
346
|
-
out.push({
|
|
347
|
-
...message,
|
|
348
|
-
embedding: vector,
|
|
349
|
-
embeddingModel: message.embeddingModel ?? "server",
|
|
350
|
-
embeddingDimensions: vector.length,
|
|
351
|
-
embeddingUpdatedAt: Date.now()
|
|
352
|
-
});
|
|
353
295
|
}
|
|
354
|
-
return out;
|
|
355
296
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
};
|
|
360
|
-
async function applyEmbedOnInsertPolicy(incoming, embedOnInsert, unified) {
|
|
361
|
-
const messages = incoming;
|
|
362
|
-
const anyMissing = incoming.some((m) => !Array.isArray(m.embedding) || m.embedding.length === 0);
|
|
363
|
-
const embedderWired = typeof unified?.embedQuery === "function";
|
|
364
|
-
if (embedOnInsert === true && anyMissing) {
|
|
365
|
-
return { messages: await embedMissingMessages(messages, unified ?? {}), warnings: [] };
|
|
366
|
-
}
|
|
367
|
-
if (embedOnInsert !== true && anyMissing && embedderWired) {
|
|
368
|
-
return {
|
|
369
|
-
messages: await embedMissingMessages(messages, unified ?? {}),
|
|
370
|
-
warnings: [AUTO_APPLIED_WARNING]
|
|
371
|
-
};
|
|
297
|
+
function resolveSearchRuntimeContext(input, runtimeContext, now = Date.now) {
|
|
298
|
+
if (runtimeContext === void 0) {
|
|
299
|
+
return void 0;
|
|
372
300
|
}
|
|
373
|
-
|
|
301
|
+
if (!isRecord(runtimeContext) || !Array.isArray(runtimeContext.applicabilityContexts)) {
|
|
302
|
+
invalidRuntimeContext("applicabilityContexts must be an array");
|
|
303
|
+
}
|
|
304
|
+
for (const [index, context] of runtimeContext.applicabilityContexts.entries()) {
|
|
305
|
+
validateApplicabilityContext(context, index);
|
|
306
|
+
}
|
|
307
|
+
let applicabilityAt;
|
|
308
|
+
if (input.asOf === void 0) {
|
|
309
|
+
applicabilityAt = now();
|
|
310
|
+
} else {
|
|
311
|
+
const parsed = typeof input.asOf === "string" ? Date.parse(input.asOf) : Number.NaN;
|
|
312
|
+
if (!Number.isFinite(parsed)) {
|
|
313
|
+
invalidRuntimeContext("asOf must be a parseable timestamp when a runtime context is supplied");
|
|
314
|
+
}
|
|
315
|
+
applicabilityAt = parsed;
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
applicabilityContexts: runtimeContext.applicabilityContexts,
|
|
319
|
+
applicabilityAt
|
|
320
|
+
};
|
|
374
321
|
}
|
|
375
322
|
|
|
376
|
-
// src/search/unified-search.ts
|
|
377
|
-
init_chroma_memory_index();
|
|
378
|
-
init_raw_message_store();
|
|
379
|
-
|
|
380
323
|
// src/search/reranker.ts
|
|
381
324
|
async function applyReranker(reranker, query, results) {
|
|
382
325
|
if (!reranker || results.length === 0) {
|
|
@@ -570,6 +513,7 @@ function materializeSignals(lists, hit) {
|
|
|
570
513
|
function listNameToChannel(name) {
|
|
571
514
|
if (name === "memory-semantic") return "semantic";
|
|
572
515
|
if (name === "memory-bm25" || name === "memory-lexical") return "lexical";
|
|
516
|
+
if (name === "memory-hybrid") return "hybrid";
|
|
573
517
|
if (name === "memory-entity") return "entity";
|
|
574
518
|
return void 0;
|
|
575
519
|
}
|
|
@@ -627,7 +571,6 @@ async function resolveScopePeer(input) {
|
|
|
627
571
|
|
|
628
572
|
// src/search/gather-evidence.ts
|
|
629
573
|
var DEFAULT_TIERS = ["summary", "raw", "insight", "knowledge"];
|
|
630
|
-
var MAX_EVIDENCE_CHARS = 600;
|
|
631
574
|
function toSummaryResult(row) {
|
|
632
575
|
const start = typeof row.startTimestamp === "number" ? row.startTimestamp : void 0;
|
|
633
576
|
const end = typeof row.endTimestamp === "number" ? row.endTimestamp : void 0;
|
|
@@ -645,7 +588,7 @@ function toSummaryResult(row) {
|
|
|
645
588
|
}
|
|
646
589
|
};
|
|
647
590
|
}
|
|
648
|
-
async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers) {
|
|
591
|
+
async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers, runtimeContext) {
|
|
649
592
|
if (typeof deps.searchSummaries !== "function") {
|
|
650
593
|
return { hits: [], warnings: [] };
|
|
651
594
|
}
|
|
@@ -658,7 +601,8 @@ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers)
|
|
|
658
601
|
limit,
|
|
659
602
|
threshold,
|
|
660
603
|
authToken: input.authToken,
|
|
661
|
-
...peerPeers.length > 0 ? { peers: peerPeers } : {}
|
|
604
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
605
|
+
...runtimeContext ?? {}
|
|
662
606
|
});
|
|
663
607
|
const hits = rows.map(toSummaryResult);
|
|
664
608
|
return { hits, warnings: [] };
|
|
@@ -676,12 +620,6 @@ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers)
|
|
|
676
620
|
};
|
|
677
621
|
}
|
|
678
622
|
}
|
|
679
|
-
function truncate(input, max = MAX_EVIDENCE_CHARS) {
|
|
680
|
-
if (input.length <= max) {
|
|
681
|
-
return input;
|
|
682
|
-
}
|
|
683
|
-
return `${input.slice(0, max - 1)}\u2026`;
|
|
684
|
-
}
|
|
685
623
|
function buildSynthesisPrompt(input) {
|
|
686
624
|
const grouped = /* @__PURE__ */ new Map();
|
|
687
625
|
for (const item of input.evidence) {
|
|
@@ -695,9 +633,10 @@ function buildSynthesisPrompt(input) {
|
|
|
695
633
|
if (items.length === 0) {
|
|
696
634
|
continue;
|
|
697
635
|
}
|
|
698
|
-
const lines = items.map(
|
|
699
|
-
|
|
700
|
-
|
|
636
|
+
const lines = items.map((item, index) => {
|
|
637
|
+
const factSource = typeof item.metadata?.source === "string" && item.metadata.source.length > 0 ? item.metadata.source : item.source ?? "unknown";
|
|
638
|
+
return ` [${index + 1}] (${item.id}, source=${factSource}, score=${item.score.toFixed(4)}) ${item.snippet}`;
|
|
639
|
+
});
|
|
701
640
|
sections.push(`## ${tier}
|
|
702
641
|
${lines.join("\n")}`);
|
|
703
642
|
}
|
|
@@ -779,12 +718,15 @@ function coerceAnswer(text, schema) {
|
|
|
779
718
|
return text;
|
|
780
719
|
}
|
|
781
720
|
async function synthesizeAnswer(input) {
|
|
782
|
-
const { query,
|
|
783
|
-
const evidence =
|
|
784
|
-
id:
|
|
785
|
-
source:
|
|
786
|
-
snippet:
|
|
787
|
-
score:
|
|
721
|
+
const { query, responseSchema, deps, logger } = input;
|
|
722
|
+
const evidence = input.evidence.map((item) => ({
|
|
723
|
+
id: item.id,
|
|
724
|
+
source: mapEvidenceSourceToTier(item.source),
|
|
725
|
+
snippet: item.snippet,
|
|
726
|
+
score: item.score,
|
|
727
|
+
// Forward per-fact provenance (e.g. `metadata.source` from
|
|
728
|
+
// `opencontext add --source`) so the synthesis prompt can cite it.
|
|
729
|
+
metadata: item.metadata
|
|
788
730
|
}));
|
|
789
731
|
const warnings = [];
|
|
790
732
|
const complete = deps.reasoning?.complete;
|
|
@@ -815,21 +757,10 @@ async function synthesizeAnswer(input) {
|
|
|
815
757
|
return { answer: "", warnings };
|
|
816
758
|
}
|
|
817
759
|
}
|
|
818
|
-
function
|
|
819
|
-
|
|
820
|
-
if (
|
|
821
|
-
|
|
822
|
-
}
|
|
823
|
-
if (hit.type === "memory") {
|
|
824
|
-
return "raw";
|
|
825
|
-
}
|
|
826
|
-
if (hit.type === "insight") {
|
|
827
|
-
return "insight";
|
|
828
|
-
}
|
|
829
|
-
if (hit.type === "knowledge") {
|
|
830
|
-
return "knowledge";
|
|
831
|
-
}
|
|
832
|
-
return hit.type;
|
|
760
|
+
function mapEvidenceSourceToTier(source) {
|
|
761
|
+
if (source === "memory") return "raw";
|
|
762
|
+
if (source === "insights") return "insight";
|
|
763
|
+
return source;
|
|
833
764
|
}
|
|
834
765
|
async function resolveSearchScopePeers(input) {
|
|
835
766
|
return resolveScopePeer({
|
|
@@ -853,7 +784,7 @@ function deriveEntityKeywords(query) {
|
|
|
853
784
|
}
|
|
854
785
|
return out.slice(0, 16);
|
|
855
786
|
}
|
|
856
|
-
async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = []) {
|
|
787
|
+
async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = [], runtimeContext) {
|
|
857
788
|
if (typeof deps.entitySearch !== "function") {
|
|
858
789
|
return [];
|
|
859
790
|
}
|
|
@@ -871,7 +802,8 @@ async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = [
|
|
|
871
802
|
keywords,
|
|
872
803
|
limit: Math.ceil(limit / filters.length),
|
|
873
804
|
botId,
|
|
874
|
-
...peerPeers.length > 0 ? { peers: peerPeers } : {}
|
|
805
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
806
|
+
...runtimeContext ?? {}
|
|
875
807
|
})
|
|
876
808
|
)
|
|
877
809
|
)).flat();
|
|
@@ -959,6 +891,16 @@ function filterByDateRange(items, dateFrom, dateTo) {
|
|
|
959
891
|
return true;
|
|
960
892
|
});
|
|
961
893
|
}
|
|
894
|
+
function warnApplicabilityNotEnforced(warnings) {
|
|
895
|
+
if (warnings.some((warning) => warning.code === "memory_applicability_not_enforced")) {
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
warnings.push({
|
|
899
|
+
source: "memory",
|
|
900
|
+
code: "memory_applicability_not_enforced",
|
|
901
|
+
message: "Built-in raw-message retrieval was skipped because it cannot enforce the supplied applicability context. Configure an applicability-aware raw-message provider."
|
|
902
|
+
});
|
|
903
|
+
}
|
|
962
904
|
async function embedQueryVariant(embedQuery, input, query) {
|
|
963
905
|
return embedQuery({
|
|
964
906
|
userId: input.userId,
|
|
@@ -966,10 +908,29 @@ async function embedQueryVariant(embedQuery, input, query) {
|
|
|
966
908
|
authToken: input.authToken
|
|
967
909
|
});
|
|
968
910
|
}
|
|
969
|
-
async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger, peerPeers
|
|
911
|
+
async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger, peerPeers, warnings, runtimeContext) {
|
|
970
912
|
const filters = input.botIds && input.botIds.length > 0 ? input.botIds.map((botId) => ({ botId })) : [{}];
|
|
971
913
|
let semantic = [];
|
|
972
|
-
if (
|
|
914
|
+
if (typeof deps.searchRawMessagesAnn === "function") {
|
|
915
|
+
const searchRawMessagesAnn = deps.searchRawMessagesAnn;
|
|
916
|
+
const factTypes = input.factTypes?.length ? input.factTypes : void 0;
|
|
917
|
+
const includeDeprecated = input.includeDeprecated === true;
|
|
918
|
+
semantic = (await Promise.all(
|
|
919
|
+
filters.map(
|
|
920
|
+
(filter) => searchRawMessagesAnn({
|
|
921
|
+
userId: input.userId,
|
|
922
|
+
queryEmbedding,
|
|
923
|
+
limit,
|
|
924
|
+
threshold,
|
|
925
|
+
botId: "botId" in filter ? filter.botId : void 0,
|
|
926
|
+
includeDeprecated,
|
|
927
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
928
|
+
...factTypes ? { factTypes } : {},
|
|
929
|
+
...runtimeContext ?? {}
|
|
930
|
+
})
|
|
931
|
+
)
|
|
932
|
+
)).flat().filter(isRawMemorySemanticResult).map(toMemoryResult);
|
|
933
|
+
} else if (runtimeContext === void 0 && isRawMessageChromaEnabled()) {
|
|
973
934
|
try {
|
|
974
935
|
semantic = (await Promise.all(
|
|
975
936
|
filters.map((filter) => {
|
|
@@ -994,24 +955,12 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
|
|
|
994
955
|
error
|
|
995
956
|
);
|
|
996
957
|
}
|
|
997
|
-
} else if (typeof deps.searchRawMessagesAnn === "function") {
|
|
998
|
-
const searchRawMessagesAnn = deps.searchRawMessagesAnn;
|
|
999
|
-
const factTypes = input.factTypes?.length ? input.factTypes : void 0;
|
|
1000
|
-
semantic = (await Promise.all(
|
|
1001
|
-
filters.map(
|
|
1002
|
-
(filter) => searchRawMessagesAnn({
|
|
1003
|
-
userId: input.userId,
|
|
1004
|
-
queryEmbedding,
|
|
1005
|
-
limit,
|
|
1006
|
-
threshold,
|
|
1007
|
-
botId: "botId" in filter ? filter.botId : void 0,
|
|
1008
|
-
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
1009
|
-
...factTypes ? { factTypes } : {}
|
|
1010
|
-
})
|
|
1011
|
-
)
|
|
1012
|
-
)).flat().filter(isRawMemorySemanticResult).map(toMemoryResult);
|
|
1013
958
|
}
|
|
1014
|
-
if (semantic.length === 0) {
|
|
959
|
+
if (semantic.length === 0 && runtimeContext !== void 0) {
|
|
960
|
+
warnApplicabilityNotEnforced(warnings);
|
|
961
|
+
return [];
|
|
962
|
+
}
|
|
963
|
+
if (semantic.length === 0 && typeof deps.searchRawMessagesAnn !== "function") {
|
|
1015
964
|
try {
|
|
1016
965
|
const { getRawMessageManager: getRawMessageManager2 } = await Promise.resolve().then(() => (init_raw_message_store(), raw_message_store_exports));
|
|
1017
966
|
const manager2 = await getRawMessageManager2();
|
|
@@ -1022,6 +971,7 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
|
|
|
1022
971
|
queryEmbedding,
|
|
1023
972
|
limit,
|
|
1024
973
|
threshold,
|
|
974
|
+
includeDeprecated: input.includeDeprecated === true,
|
|
1025
975
|
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
1026
976
|
...factTypes ? { factTypes } : {}
|
|
1027
977
|
});
|
|
@@ -1051,6 +1001,18 @@ function mergeByMaxScore(lists) {
|
|
|
1051
1001
|
}
|
|
1052
1002
|
return Array.from(best.values()).sort((a, b) => b.similarity - a.similarity);
|
|
1053
1003
|
}
|
|
1004
|
+
function dedupeChannelByParent(hits) {
|
|
1005
|
+
const strongest = /* @__PURE__ */ new Map();
|
|
1006
|
+
for (const hit of hits) {
|
|
1007
|
+
const sourceMessageId = hit.metadata?.sourceMessageId;
|
|
1008
|
+
const parentId = typeof sourceMessageId === "string" && sourceMessageId.length > 0 ? sourceMessageId : hit.id;
|
|
1009
|
+
const normalized = hit.type === "memory" && hit.id !== parentId ? { ...hit, id: parentId } : hit;
|
|
1010
|
+
const key = `${normalized.type}:${parentId}`;
|
|
1011
|
+
const current = strongest.get(key);
|
|
1012
|
+
if (!current || normalized.similarity > current.similarity) strongest.set(key, normalized);
|
|
1013
|
+
}
|
|
1014
|
+
return [...strongest.values()].sort((a, b) => b.similarity - a.similarity);
|
|
1015
|
+
}
|
|
1054
1016
|
function toIterativeRecallCandidate(hit) {
|
|
1055
1017
|
return {
|
|
1056
1018
|
id: hit.id,
|
|
@@ -1059,7 +1021,7 @@ function toIterativeRecallCandidate(hit) {
|
|
|
1059
1021
|
metadata: hit.metadata
|
|
1060
1022
|
};
|
|
1061
1023
|
}
|
|
1062
|
-
async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger, peerPeers
|
|
1024
|
+
async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger, peerPeers, warnings, runtimeContext) {
|
|
1063
1025
|
if (keywords.length === 0) {
|
|
1064
1026
|
return [];
|
|
1065
1027
|
}
|
|
@@ -1068,6 +1030,7 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
|
|
|
1068
1030
|
const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
|
|
1069
1031
|
const searchRawMessagesLexical = deps.searchRawMessagesLexical;
|
|
1070
1032
|
const factTypes = input.factTypes?.length ? input.factTypes : void 0;
|
|
1033
|
+
const includeDeprecated = input.includeDeprecated === true;
|
|
1071
1034
|
return (await Promise.all(
|
|
1072
1035
|
filters.map(
|
|
1073
1036
|
(botId) => searchRawMessagesLexical({
|
|
@@ -1075,8 +1038,10 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
|
|
|
1075
1038
|
keywords,
|
|
1076
1039
|
limit: Math.ceil(limit / filters.length),
|
|
1077
1040
|
botId,
|
|
1041
|
+
includeDeprecated,
|
|
1078
1042
|
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
1079
|
-
...factTypes ? { factTypes } : {}
|
|
1043
|
+
...factTypes ? { factTypes } : {},
|
|
1044
|
+
...runtimeContext ?? {}
|
|
1080
1045
|
})
|
|
1081
1046
|
)
|
|
1082
1047
|
)).flat().filter(isRawMemorySemanticResult).map((hit) => ({
|
|
@@ -1087,10 +1052,15 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
|
|
|
1087
1052
|
logger.warn?.("[memory-store] lexical memory search failed:", error);
|
|
1088
1053
|
}
|
|
1089
1054
|
}
|
|
1055
|
+
if (runtimeContext !== void 0) {
|
|
1056
|
+
warnApplicabilityNotEnforced(warnings);
|
|
1057
|
+
return [];
|
|
1058
|
+
}
|
|
1090
1059
|
try {
|
|
1091
1060
|
const { lexicalSearchRawMessages: lexicalSearchRawMessages2 } = await Promise.resolve().then(() => (init_sqlite_raw_message_store(), sqlite_raw_message_store_exports));
|
|
1092
1061
|
const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
|
|
1093
1062
|
const factTypes = input.factTypes?.length ? input.factTypes : void 0;
|
|
1063
|
+
const includeDeprecated = input.includeDeprecated === true;
|
|
1094
1064
|
return (await Promise.all(
|
|
1095
1065
|
filters.map(
|
|
1096
1066
|
(botId) => lexicalSearchRawMessages2({
|
|
@@ -1098,6 +1068,7 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
|
|
|
1098
1068
|
keywords,
|
|
1099
1069
|
limit: Math.ceil(limit / filters.length),
|
|
1100
1070
|
botId,
|
|
1071
|
+
includeDeprecated,
|
|
1101
1072
|
...factTypes ? { factTypes } : {}
|
|
1102
1073
|
})
|
|
1103
1074
|
)
|
|
@@ -1128,7 +1099,9 @@ function searchInputToUnified(input) {
|
|
|
1128
1099
|
mergeStrategy: input.mergeStrategy,
|
|
1129
1100
|
peerFilter: input.peerFilter,
|
|
1130
1101
|
reasoningStrategy: input.reasoningStrategy,
|
|
1131
|
-
factTypes: input.factTypes
|
|
1102
|
+
factTypes: input.factTypes,
|
|
1103
|
+
includeRetrievalDiagnostics: input.includeRetrievalDiagnostics,
|
|
1104
|
+
includeDeprecated: input.includeDeprecated
|
|
1132
1105
|
};
|
|
1133
1106
|
}
|
|
1134
1107
|
function mapHitToEvidenceSource(hit) {
|
|
@@ -1156,7 +1129,7 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1156
1129
|
}
|
|
1157
1130
|
return normalizeUnifiedMemoryReasoningStrategy(deps.reasoning?.defaultStrategy, "none");
|
|
1158
1131
|
}
|
|
1159
|
-
async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = []) {
|
|
1132
|
+
async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = [], runtimeContext) {
|
|
1160
1133
|
if ((reasoningStrategy === "iterative" || reasoningStrategy === "union") && !deps.reasoning?.iterativePlanner) {
|
|
1161
1134
|
warnings.push({
|
|
1162
1135
|
source: "memory",
|
|
@@ -1189,7 +1162,9 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1189
1162
|
keywords2,
|
|
1190
1163
|
limit,
|
|
1191
1164
|
logger,
|
|
1192
|
-
peerPeers
|
|
1165
|
+
peerPeers,
|
|
1166
|
+
warnings,
|
|
1167
|
+
runtimeContext
|
|
1193
1168
|
);
|
|
1194
1169
|
let semanticHits = [];
|
|
1195
1170
|
if (typeof deps.embedQuery === "function") {
|
|
@@ -1203,7 +1178,9 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1203
1178
|
limit,
|
|
1204
1179
|
threshold,
|
|
1205
1180
|
logger,
|
|
1206
|
-
peerPeers
|
|
1181
|
+
peerPeers,
|
|
1182
|
+
warnings,
|
|
1183
|
+
runtimeContext
|
|
1207
1184
|
);
|
|
1208
1185
|
} catch (error) {
|
|
1209
1186
|
logger.warn?.(
|
|
@@ -1244,6 +1221,11 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1244
1221
|
unionEvidence = evidence;
|
|
1245
1222
|
}
|
|
1246
1223
|
if (typeof deps.embedQuery !== "function") {
|
|
1224
|
+
warnings.push({
|
|
1225
|
+
source: "memory",
|
|
1226
|
+
code: "semantic_unavailable",
|
|
1227
|
+
message: "Semantic search is unavailable because no embedding provider is configured."
|
|
1228
|
+
});
|
|
1247
1229
|
warnings.push({
|
|
1248
1230
|
source: "memory",
|
|
1249
1231
|
code: "memory_lexical_search_fallback",
|
|
@@ -1251,7 +1233,16 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1251
1233
|
});
|
|
1252
1234
|
const keywords2 = deriveLexicalKeywords(input.query);
|
|
1253
1235
|
const lexical2 = filterByDateRange(
|
|
1254
|
-
await runLexicalSearchForKeywords(
|
|
1236
|
+
await runLexicalSearchForKeywords(
|
|
1237
|
+
deps,
|
|
1238
|
+
input,
|
|
1239
|
+
keywords2,
|
|
1240
|
+
limit,
|
|
1241
|
+
logger,
|
|
1242
|
+
peerPeers,
|
|
1243
|
+
warnings,
|
|
1244
|
+
runtimeContext
|
|
1245
|
+
),
|
|
1255
1246
|
input.dateFrom,
|
|
1256
1247
|
input.dateTo
|
|
1257
1248
|
);
|
|
@@ -1259,6 +1250,26 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1259
1250
|
}
|
|
1260
1251
|
let semantic = [];
|
|
1261
1252
|
const embedQuery = deps.embedQuery;
|
|
1253
|
+
const activeMergeStrategy = normalizeUnifiedMemoryMergeStrategy(
|
|
1254
|
+
input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
|
|
1255
|
+
);
|
|
1256
|
+
if (reasoningStrategy === "none" && activeMergeStrategy === "rrf" && input.threshold === void 0 && typeof deps.searchRawMessagesHybrid === "function") {
|
|
1257
|
+
const queryEmbedding = await embedQueryVariant(embedQuery, input, input.query);
|
|
1258
|
+
const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
|
|
1259
|
+
const hybrid = (await Promise.all(
|
|
1260
|
+
filters.map(
|
|
1261
|
+
(botId) => deps.searchRawMessagesHybrid?.({
|
|
1262
|
+
userId: input.userId,
|
|
1263
|
+
query: input.query,
|
|
1264
|
+
queryEmbedding,
|
|
1265
|
+
limit: Math.ceil(limit / filters.length),
|
|
1266
|
+
botId,
|
|
1267
|
+
...runtimeContext ?? {}
|
|
1268
|
+
})
|
|
1269
|
+
)
|
|
1270
|
+
)).flatMap((items) => items ?? []).filter(isRawMemorySemanticResult).map(toMemoryResult);
|
|
1271
|
+
return { semantic: [], lexical: [], hybrid: filterByDateRange(hybrid, input.dateFrom, input.dateTo) };
|
|
1272
|
+
}
|
|
1262
1273
|
if (reasoningStrategy === "rewrite" && deps.reasoning?.queryRewriter) {
|
|
1263
1274
|
try {
|
|
1264
1275
|
const variants = await deps.reasoning.queryRewriter.rewrite({
|
|
@@ -1277,7 +1288,17 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1277
1288
|
);
|
|
1278
1289
|
const lists = await Promise.all(
|
|
1279
1290
|
embeddings.map(
|
|
1280
|
-
(embedding) => runSemanticSearchForEmbedding(
|
|
1291
|
+
(embedding) => runSemanticSearchForEmbedding(
|
|
1292
|
+
deps,
|
|
1293
|
+
input,
|
|
1294
|
+
embedding,
|
|
1295
|
+
limit,
|
|
1296
|
+
threshold,
|
|
1297
|
+
logger,
|
|
1298
|
+
peerPeers,
|
|
1299
|
+
warnings,
|
|
1300
|
+
runtimeContext
|
|
1301
|
+
)
|
|
1281
1302
|
)
|
|
1282
1303
|
);
|
|
1283
1304
|
semantic = mergeByMaxScore(lists);
|
|
@@ -1300,7 +1321,9 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1300
1321
|
limit,
|
|
1301
1322
|
threshold,
|
|
1302
1323
|
logger,
|
|
1303
|
-
peerPeers
|
|
1324
|
+
peerPeers,
|
|
1325
|
+
warnings,
|
|
1326
|
+
runtimeContext
|
|
1304
1327
|
);
|
|
1305
1328
|
}
|
|
1306
1329
|
let lexical = [];
|
|
@@ -1319,7 +1342,8 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1319
1342
|
limit,
|
|
1320
1343
|
botId,
|
|
1321
1344
|
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
1322
|
-
...factTypes ? { factTypes } : {}
|
|
1345
|
+
...factTypes ? { factTypes } : {},
|
|
1346
|
+
...runtimeContext ?? {}
|
|
1323
1347
|
})
|
|
1324
1348
|
)
|
|
1325
1349
|
)).flat().filter(isRawMemorySemanticResult).map((hit) => ({
|
|
@@ -1345,7 +1369,7 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1345
1369
|
let entity;
|
|
1346
1370
|
if (typeof deps.entitySearch === "function") {
|
|
1347
1371
|
entity = filterByDateRange(
|
|
1348
|
-
await runEntitySearchForQuery(deps, input, limit, logger, peerPeers),
|
|
1372
|
+
await runEntitySearchForQuery(deps, input, limit, logger, peerPeers, runtimeContext),
|
|
1349
1373
|
input.dateFrom,
|
|
1350
1374
|
input.dateTo
|
|
1351
1375
|
);
|
|
@@ -1407,14 +1431,16 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1407
1431
|
}
|
|
1408
1432
|
return out;
|
|
1409
1433
|
}
|
|
1410
|
-
async function
|
|
1434
|
+
async function searchUnifiedMemoryWithRuntime(input, runtimeContext) {
|
|
1411
1435
|
const query = input.query.trim();
|
|
1412
1436
|
const sources = normalizeUnifiedMemorySearchSources(input.sources);
|
|
1413
1437
|
const limit = clampUnifiedMemorySearchLimit(input.limit);
|
|
1414
|
-
const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
|
|
1415
1438
|
const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
|
|
1416
1439
|
input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
|
|
1417
1440
|
);
|
|
1441
|
+
const candidateLimit = Math.min(50, Math.max(limit, limit * 4));
|
|
1442
|
+
const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
|
|
1443
|
+
const memoryThreshold = input.threshold === void 0 && mergeStrategy === "rrf" ? Number.NEGATIVE_INFINITY : threshold;
|
|
1418
1444
|
const reasoningStrategy = resolveReasoningStrategy(input);
|
|
1419
1445
|
const warnings = [];
|
|
1420
1446
|
const peerScope = input.peerFilter && input.peerFilter.length > 0 ? await resolveScopePeer({
|
|
@@ -1434,23 +1460,46 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1434
1460
|
};
|
|
1435
1461
|
}
|
|
1436
1462
|
let memorySubs;
|
|
1463
|
+
let retrievalStatus;
|
|
1437
1464
|
const reasoningInfo = { strategy: reasoningStrategy };
|
|
1438
1465
|
if (input.dateFrom || input.dateTo) {
|
|
1439
1466
|
reasoningInfo.dateRange = { from: input.dateFrom, to: input.dateTo };
|
|
1440
1467
|
}
|
|
1441
1468
|
if (sources.includes("memory")) {
|
|
1442
|
-
const hasRawProviders = typeof deps.searchRawMessagesAnn === "function" || typeof deps.searchRawMessagesLexical === "function";
|
|
1469
|
+
const hasRawProviders = typeof deps.searchRawMessagesAnn === "function" || typeof deps.searchRawMessagesHybrid === "function" || typeof deps.searchRawMessagesLexical === "function";
|
|
1443
1470
|
if (isRawMessageStorageAvailable() || hasRawProviders) {
|
|
1444
1471
|
try {
|
|
1445
1472
|
memorySubs = await runMemorySource(
|
|
1446
1473
|
input,
|
|
1447
|
-
|
|
1448
|
-
|
|
1474
|
+
candidateLimit,
|
|
1475
|
+
memoryThreshold,
|
|
1449
1476
|
warnings,
|
|
1450
1477
|
reasoningStrategy,
|
|
1451
1478
|
reasoningInfo,
|
|
1452
|
-
peerPeers
|
|
1479
|
+
peerPeers,
|
|
1480
|
+
runtimeContext
|
|
1453
1481
|
);
|
|
1482
|
+
memorySubs = {
|
|
1483
|
+
semantic: dedupeChannelByParent(memorySubs.semantic),
|
|
1484
|
+
lexical: dedupeChannelByParent(memorySubs.lexical),
|
|
1485
|
+
...memorySubs.hybrid ? { hybrid: dedupeChannelByParent(memorySubs.hybrid) } : {},
|
|
1486
|
+
...memorySubs.entity ? { entity: dedupeChannelByParent(memorySubs.entity) } : {}
|
|
1487
|
+
};
|
|
1488
|
+
if (deps.getRawMessageRetrievalStatus) {
|
|
1489
|
+
retrievalStatus = await deps.getRawMessageRetrievalStatus();
|
|
1490
|
+
}
|
|
1491
|
+
if (memorySubs.semantic.length === 0 && (memorySubs.hybrid?.length ?? 0) === 0 && typeof deps.embedQuery === "function") {
|
|
1492
|
+
let code = "semantic_candidates_empty";
|
|
1493
|
+
let message = "Semantic search was available but returned no candidates.";
|
|
1494
|
+
if (retrievalStatus?.embeddedChildCount === 0) {
|
|
1495
|
+
code = "semantic_unavailable";
|
|
1496
|
+
message = "No embedded RawMessage children are available for semantic search.";
|
|
1497
|
+
} else if (retrievalStatus?.embeddingDimensions && retrievalStatus.indexedDimensions.length > 0 && !retrievalStatus.indexedDimensions.includes(retrievalStatus.embeddingDimensions)) {
|
|
1498
|
+
code = "semantic_dimension_mismatch";
|
|
1499
|
+
message = `Query embedding dimension ${retrievalStatus.embeddingDimensions} does not match indexed child dimensions ${retrievalStatus.indexedDimensions.join(", ")}.`;
|
|
1500
|
+
}
|
|
1501
|
+
warnings.push({ source: "memory", code, message });
|
|
1502
|
+
}
|
|
1454
1503
|
} catch (error) {
|
|
1455
1504
|
logger.warn?.("[memory-store] memory source failed:", error);
|
|
1456
1505
|
warnings.push({
|
|
@@ -1473,12 +1522,13 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1473
1522
|
const hits = await deps.searchInsights({
|
|
1474
1523
|
userId: input.userId,
|
|
1475
1524
|
query: input.query,
|
|
1476
|
-
limit,
|
|
1525
|
+
limit: candidateLimit,
|
|
1477
1526
|
threshold,
|
|
1478
1527
|
botIds: input.botIds,
|
|
1479
1528
|
includeArchived: input.includeArchivedInsights,
|
|
1480
1529
|
authToken: input.authToken,
|
|
1481
|
-
...peerPeers.length > 0 ? { peers: peerPeers } : {}
|
|
1530
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
1531
|
+
...runtimeContext ?? {}
|
|
1482
1532
|
});
|
|
1483
1533
|
for (const hit of hits) {
|
|
1484
1534
|
insightHits.push({
|
|
@@ -1505,12 +1555,13 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1505
1555
|
userId: input.userId,
|
|
1506
1556
|
query: input.query,
|
|
1507
1557
|
options: {
|
|
1508
|
-
limit,
|
|
1558
|
+
limit: candidateLimit,
|
|
1509
1559
|
threshold,
|
|
1510
1560
|
documentIds: input.documentIds
|
|
1511
1561
|
},
|
|
1512
1562
|
authToken: input.authToken,
|
|
1513
|
-
...peerPeers.length > 0 ? { peers: peerPeers } : {}
|
|
1563
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
1564
|
+
...runtimeContext ?? {}
|
|
1514
1565
|
});
|
|
1515
1566
|
for (const hit of hits) {
|
|
1516
1567
|
knowledgeHits.push(toKnowledgeResult(hit));
|
|
@@ -1528,10 +1579,14 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1528
1579
|
memorySubs,
|
|
1529
1580
|
insightHits,
|
|
1530
1581
|
knowledgeHits,
|
|
1531
|
-
limit,
|
|
1582
|
+
limit: candidateLimit,
|
|
1532
1583
|
strategy: mergeStrategy
|
|
1533
1584
|
});
|
|
1534
|
-
const
|
|
1585
|
+
const rerankerStartedAt = deps.reranker ? Date.now() : void 0;
|
|
1586
|
+
const reranked = await applyReranker(deps.reranker, input.query, merged);
|
|
1587
|
+
const rerankerLatencyMs = rerankerStartedAt === void 0 ? 0 : Date.now() - rerankerStartedAt;
|
|
1588
|
+
const rerankerOrderChanged = Boolean(deps.reranker) && merged.some((hit, index) => hit.id !== reranked[index]?.id);
|
|
1589
|
+
const ranked = reranked.slice(0, limit);
|
|
1535
1590
|
const output = {
|
|
1536
1591
|
query: input.query,
|
|
1537
1592
|
sources,
|
|
@@ -1539,28 +1594,51 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1539
1594
|
count: ranked.length,
|
|
1540
1595
|
warnings
|
|
1541
1596
|
};
|
|
1597
|
+
if (input.includeRetrievalDiagnostics) {
|
|
1598
|
+
output.retrievalDiagnostics = {
|
|
1599
|
+
mergeStrategy,
|
|
1600
|
+
candidateLimit,
|
|
1601
|
+
backend: retrievalStatus?.backend,
|
|
1602
|
+
semanticDegradedReason: retrievalStatus?.semanticDegradedReason ?? warnings.find((warning) => warning.code.startsWith("semantic_"))?.code,
|
|
1603
|
+
candidateCounts: {
|
|
1604
|
+
semantic: memorySubs?.semantic.length ?? 0,
|
|
1605
|
+
lexical: memorySubs?.lexical.length ?? 0,
|
|
1606
|
+
hybrid: memorySubs?.hybrid?.length ?? 0,
|
|
1607
|
+
entity: memorySubs?.entity?.length ?? 0,
|
|
1608
|
+
fused: merged.length,
|
|
1609
|
+
final: ranked.length
|
|
1610
|
+
},
|
|
1611
|
+
channels: {
|
|
1612
|
+
semantic: memorySubs?.semantic ?? [],
|
|
1613
|
+
lexical: memorySubs?.lexical ?? [],
|
|
1614
|
+
...memorySubs?.hybrid ? { hybrid: memorySubs.hybrid } : {},
|
|
1615
|
+
...memorySubs?.entity ? { entity: memorySubs.entity } : {}
|
|
1616
|
+
},
|
|
1617
|
+
fusedBeforeRerank: merged,
|
|
1618
|
+
reranker: {
|
|
1619
|
+
enabled: Boolean(deps.reranker),
|
|
1620
|
+
provider: deps.rerankerInfo?.provider,
|
|
1621
|
+
model: deps.rerankerInfo?.model,
|
|
1622
|
+
inputCount: merged.length,
|
|
1623
|
+
outputCount: reranked.length,
|
|
1624
|
+
latencyMs: rerankerLatencyMs,
|
|
1625
|
+
orderChanged: rerankerOrderChanged
|
|
1626
|
+
},
|
|
1627
|
+
final: ranked
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1542
1630
|
if (reasoningInfo && (reasoningInfo.strategy !== "none" || reasoningInfo.dateRange !== void 0)) {
|
|
1543
1631
|
output.reasoning = reasoningInfo;
|
|
1544
1632
|
}
|
|
1545
1633
|
return output;
|
|
1546
1634
|
}
|
|
1635
|
+
async function searchUnifiedMemory(input) {
|
|
1636
|
+
return searchUnifiedMemoryWithRuntime(input);
|
|
1637
|
+
}
|
|
1547
1638
|
async function searchRawMemorySemantically(input) {
|
|
1548
|
-
|
|
1549
|
-
const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
|
|
1550
|
-
const reasoningStrategy = resolveReasoningStrategy(input);
|
|
1551
|
-
const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
|
|
1552
|
-
input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
|
|
1553
|
-
);
|
|
1554
|
-
const sub = await runMemorySource(input, limit, threshold, [], reasoningStrategy);
|
|
1555
|
-
return mergeAcrossSources({
|
|
1556
|
-
memorySubs: sub,
|
|
1557
|
-
insightHits: [],
|
|
1558
|
-
knowledgeHits: [],
|
|
1559
|
-
limit,
|
|
1560
|
-
strategy: mergeStrategy
|
|
1561
|
-
});
|
|
1639
|
+
return (await searchUnifiedMemory({ ...input, sources: ["memory"] })).results;
|
|
1562
1640
|
}
|
|
1563
|
-
async function
|
|
1641
|
+
async function searchWithResolvedRuntime(input, runtimeContext) {
|
|
1564
1642
|
const query = input.query.trim();
|
|
1565
1643
|
const wantsSynthesis = typeof input.synthesize === "boolean" ? input.synthesize : typeof input.synthesize === "object" && input.synthesize !== null;
|
|
1566
1644
|
const synthesisSchema = typeof input.synthesize === "object" && input.synthesize !== null ? input.synthesize.responseSchema ?? input.responseSchema : input.responseSchema;
|
|
@@ -1592,19 +1670,24 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1592
1670
|
let unifiedHits = [];
|
|
1593
1671
|
let sources = input.sources ? [...input.sources] : [];
|
|
1594
1672
|
let unifiedReasoning;
|
|
1673
|
+
let retrievalDiagnostics;
|
|
1595
1674
|
if (wantsRawTier || wantsInsightTier || wantsKnowledgeTier) {
|
|
1596
1675
|
const inferredSources = sources.length > 0 ? sources : [
|
|
1597
1676
|
...wantsRawTier ? ["memory"] : [],
|
|
1598
1677
|
...wantsInsightTier ? ["insights"] : [],
|
|
1599
1678
|
...wantsKnowledgeTier ? ["knowledge"] : []
|
|
1600
1679
|
];
|
|
1601
|
-
const unified = await
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1680
|
+
const unified = await searchUnifiedMemoryWithRuntime(
|
|
1681
|
+
{
|
|
1682
|
+
...searchInputToUnified(input),
|
|
1683
|
+
sources: inferredSources.length > 0 ? inferredSources : void 0
|
|
1684
|
+
},
|
|
1685
|
+
runtimeContext
|
|
1686
|
+
);
|
|
1605
1687
|
unifiedHits = unified.results;
|
|
1606
1688
|
sources = unified.sources;
|
|
1607
1689
|
unifiedReasoning = unified.reasoning;
|
|
1690
|
+
retrievalDiagnostics = unified.retrievalDiagnostics;
|
|
1608
1691
|
warnings.push(...unified.warnings);
|
|
1609
1692
|
}
|
|
1610
1693
|
let summaryHits = [];
|
|
@@ -1615,7 +1698,8 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1615
1698
|
limit,
|
|
1616
1699
|
threshold,
|
|
1617
1700
|
logger,
|
|
1618
|
-
peerScope.peers
|
|
1701
|
+
peerScope.peers,
|
|
1702
|
+
runtimeContext
|
|
1619
1703
|
);
|
|
1620
1704
|
warnings.push(...summaryBucket.warnings);
|
|
1621
1705
|
summaryHits = summaryBucket.hits;
|
|
@@ -1640,7 +1724,8 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1640
1724
|
source: mapHitToEvidenceSource(hit),
|
|
1641
1725
|
snippet: hit.content,
|
|
1642
1726
|
score: hit.similarity,
|
|
1643
|
-
timestamp: getCandidateTimestamp(hit.metadata)
|
|
1727
|
+
timestamp: getCandidateTimestamp(hit.metadata),
|
|
1728
|
+
metadata: hit.metadata
|
|
1644
1729
|
}));
|
|
1645
1730
|
const base = {
|
|
1646
1731
|
query,
|
|
@@ -1653,12 +1738,15 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1653
1738
|
if (unifiedReasoning) {
|
|
1654
1739
|
base.reasoning = unifiedReasoning;
|
|
1655
1740
|
}
|
|
1741
|
+
if (retrievalDiagnostics) {
|
|
1742
|
+
base.retrievalDiagnostics = retrievalDiagnostics;
|
|
1743
|
+
}
|
|
1656
1744
|
if (!wantsSynthesis) {
|
|
1657
1745
|
return base;
|
|
1658
1746
|
}
|
|
1659
1747
|
const { answer, warnings: synthWarnings } = await synthesizeAnswer({
|
|
1660
1748
|
query,
|
|
1661
|
-
|
|
1749
|
+
evidence,
|
|
1662
1750
|
responseSchema: synthesisSchema,
|
|
1663
1751
|
deps,
|
|
1664
1752
|
logger
|
|
@@ -1667,6 +1755,10 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1667
1755
|
base.warnings = [...base.warnings, ...synthWarnings];
|
|
1668
1756
|
return base;
|
|
1669
1757
|
}
|
|
1758
|
+
async function search(input, runtimeContext) {
|
|
1759
|
+
const resolvedRuntime = resolveSearchRuntimeContext(input, runtimeContext);
|
|
1760
|
+
return searchWithResolvedRuntime(input, resolvedRuntime);
|
|
1761
|
+
}
|
|
1670
1762
|
return {
|
|
1671
1763
|
search,
|
|
1672
1764
|
searchUnifiedMemory,
|
|
@@ -1682,6 +1774,7 @@ function mergeAcrossSources(input) {
|
|
|
1682
1774
|
for (const hit of [
|
|
1683
1775
|
...memorySubs.semantic,
|
|
1684
1776
|
...memorySubs.lexical,
|
|
1777
|
+
...memorySubs.hybrid ?? [],
|
|
1685
1778
|
...input.insightHits,
|
|
1686
1779
|
...input.knowledgeHits
|
|
1687
1780
|
]) {
|
|
@@ -1697,6 +1790,7 @@ function mergeAcrossSources(input) {
|
|
|
1697
1790
|
const all = [
|
|
1698
1791
|
...memorySubs.semantic,
|
|
1699
1792
|
...memorySubs.lexical,
|
|
1793
|
+
...memorySubs.hybrid ?? [],
|
|
1700
1794
|
...memorySubs.entity ?? [],
|
|
1701
1795
|
...input.insightHits,
|
|
1702
1796
|
...input.knowledgeHits
|
|
@@ -1708,6 +1802,9 @@ function buildChannelLists(memorySubs, insightHits, knowledgeHits) {
|
|
|
1708
1802
|
const lists = [];
|
|
1709
1803
|
if (memorySubs.semantic.length > 0) lists.push({ name: "memory-semantic", hits: memorySubs.semantic });
|
|
1710
1804
|
if (memorySubs.lexical.length > 0) lists.push({ name: "memory-bm25", hits: memorySubs.lexical });
|
|
1805
|
+
if (memorySubs.hybrid && memorySubs.hybrid.length > 0) {
|
|
1806
|
+
lists.push({ name: "memory-hybrid", hits: memorySubs.hybrid });
|
|
1807
|
+
}
|
|
1711
1808
|
if (memorySubs.entity && memorySubs.entity.length > 0) {
|
|
1712
1809
|
lists.push({ name: "memory-entity", hits: memorySubs.entity });
|
|
1713
1810
|
}
|
|
@@ -2172,8 +2269,250 @@ function createVsaRecall(storage) {
|
|
|
2172
2269
|
};
|
|
2173
2270
|
}
|
|
2174
2271
|
|
|
2272
|
+
// ../shared/src/tokens.ts
|
|
2273
|
+
var estimateTokens = (text) => {
|
|
2274
|
+
const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length;
|
|
2275
|
+
const otherChars = text.length - chineseChars;
|
|
2276
|
+
const wordCount = Math.ceil(otherChars / 5);
|
|
2277
|
+
return chineseChars + wordCount;
|
|
2278
|
+
};
|
|
2279
|
+
|
|
2280
|
+
// ../shared/src/text-chunking.ts
|
|
2281
|
+
var RAW_MESSAGE_CHUNK_MAX_TOKENS = 400;
|
|
2282
|
+
var RAW_MESSAGE_CHUNK_OVERLAP_TOKENS = 80;
|
|
2283
|
+
var PARAGRAPH_BOUNDARIES = ["\n\n", "\r\n\r\n"];
|
|
2284
|
+
var SENTENCE_BOUNDARY = /[.!?。!?;;]\s*$/u;
|
|
2285
|
+
function largestEndWithinTokenBudget(text, start, maxTokens) {
|
|
2286
|
+
let low = start + 1;
|
|
2287
|
+
let high = text.length;
|
|
2288
|
+
let best = low;
|
|
2289
|
+
while (low <= high) {
|
|
2290
|
+
const middle = Math.floor((low + high) / 2);
|
|
2291
|
+
if (estimateTokens(text.slice(start, middle)) <= maxTokens) {
|
|
2292
|
+
best = middle;
|
|
2293
|
+
low = middle + 1;
|
|
2294
|
+
} else {
|
|
2295
|
+
high = middle - 1;
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
return best;
|
|
2299
|
+
}
|
|
2300
|
+
function preferNaturalEnd(text, start, hardEnd) {
|
|
2301
|
+
if (hardEnd >= text.length) return text.length;
|
|
2302
|
+
const searchStart = start + Math.floor((hardEnd - start) * 0.6);
|
|
2303
|
+
const candidate = text.slice(searchStart, hardEnd);
|
|
2304
|
+
for (const boundary of PARAGRAPH_BOUNDARIES) {
|
|
2305
|
+
const index = candidate.lastIndexOf(boundary);
|
|
2306
|
+
if (index >= 0) return searchStart + index + boundary.length;
|
|
2307
|
+
}
|
|
2308
|
+
for (let offset = candidate.length; offset > 0; offset -= 1) {
|
|
2309
|
+
const prefix = candidate.slice(0, offset);
|
|
2310
|
+
if (SENTENCE_BOUNDARY.test(prefix)) return searchStart + offset;
|
|
2311
|
+
}
|
|
2312
|
+
for (let offset = candidate.length - 1; offset >= 0; offset -= 1) {
|
|
2313
|
+
if (/\s/u.test(candidate[offset] ?? "")) return searchStart + offset + 1;
|
|
2314
|
+
}
|
|
2315
|
+
return hardEnd;
|
|
2316
|
+
}
|
|
2317
|
+
function overlapStart(text, chunkStart, chunkEnd, overlapTokens) {
|
|
2318
|
+
if (overlapTokens <= 0) return chunkEnd;
|
|
2319
|
+
let low = chunkStart + 1;
|
|
2320
|
+
let high = chunkEnd;
|
|
2321
|
+
let best = chunkEnd;
|
|
2322
|
+
while (low <= high) {
|
|
2323
|
+
const middle = Math.floor((low + high) / 2);
|
|
2324
|
+
if (estimateTokens(text.slice(middle, chunkEnd)) <= overlapTokens) {
|
|
2325
|
+
best = middle;
|
|
2326
|
+
high = middle - 1;
|
|
2327
|
+
} else {
|
|
2328
|
+
low = middle + 1;
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
return Math.min(chunkEnd, Math.max(chunkStart + 1, best));
|
|
2332
|
+
}
|
|
2333
|
+
function chunkTextByEstimatedTokens(text, options = {}) {
|
|
2334
|
+
if (text.length === 0) return [];
|
|
2335
|
+
const maxTokens = Math.max(1, Math.floor(options.maxTokens ?? RAW_MESSAGE_CHUNK_MAX_TOKENS));
|
|
2336
|
+
const overlapTokens = Math.max(0, Math.floor(options.overlapTokens ?? RAW_MESSAGE_CHUNK_OVERLAP_TOKENS));
|
|
2337
|
+
if (overlapTokens >= maxTokens) {
|
|
2338
|
+
throw new Error("overlapTokens must be smaller than maxTokens");
|
|
2339
|
+
}
|
|
2340
|
+
const chunks = [];
|
|
2341
|
+
let start = 0;
|
|
2342
|
+
while (start < text.length) {
|
|
2343
|
+
const hardEnd = largestEndWithinTokenBudget(text, start, maxTokens);
|
|
2344
|
+
const end = preferNaturalEnd(text, start, hardEnd);
|
|
2345
|
+
chunks.push({
|
|
2346
|
+
chunkIndex: chunks.length,
|
|
2347
|
+
startPosition: start,
|
|
2348
|
+
endPosition: end,
|
|
2349
|
+
content: text.slice(start, end)
|
|
2350
|
+
});
|
|
2351
|
+
if (end >= text.length) break;
|
|
2352
|
+
start = overlapStart(text, start, end, overlapTokens);
|
|
2353
|
+
}
|
|
2354
|
+
return chunks;
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
// src/embed-on-insert.ts
|
|
2358
|
+
var AUTO_APPLIED_WARNING = {
|
|
2359
|
+
code: "embed_on_insert_auto_applied",
|
|
2360
|
+
message: "Caller did not explicitly enable embedOnInsert, but an embedding provider is configured. OpenContext generated the missing search embeddings."
|
|
2361
|
+
};
|
|
2362
|
+
var SEMANTIC_UNAVAILABLE_WARNING = {
|
|
2363
|
+
code: "semantic_index_unavailable",
|
|
2364
|
+
message: "No embedding provider is configured. RawMessage children were indexed for lexical search only; semantic search is unavailable until they are explicitly reindexed."
|
|
2365
|
+
};
|
|
2366
|
+
function sha256(value) {
|
|
2367
|
+
return createHash("sha256").update(value).digest("hex");
|
|
2368
|
+
}
|
|
2369
|
+
function isUsableEmbedding(embedding) {
|
|
2370
|
+
return Boolean(
|
|
2371
|
+
embedding?.length && embedding.every(Number.isFinite) && embedding.some((value) => value !== 0)
|
|
2372
|
+
);
|
|
2373
|
+
}
|
|
2374
|
+
async function embedText(text, userId, deps) {
|
|
2375
|
+
if (typeof deps.embedQuery !== "function" || text.length === 0) return void 0;
|
|
2376
|
+
const embedding = await deps.embedQuery({ userId, query: text });
|
|
2377
|
+
return isUsableEmbedding(embedding) ? embedding : void 0;
|
|
2378
|
+
}
|
|
2379
|
+
async function embedMissingMessages(messages, deps) {
|
|
2380
|
+
if (typeof deps.embedQuery !== "function") return messages;
|
|
2381
|
+
const out = [];
|
|
2382
|
+
for (const message of messages) {
|
|
2383
|
+
if (isUsableEmbedding(message.embedding) || !message.content) {
|
|
2384
|
+
out.push(message);
|
|
2385
|
+
continue;
|
|
2386
|
+
}
|
|
2387
|
+
const embedding = await embedText(message.content, message.userId, deps);
|
|
2388
|
+
out.push(
|
|
2389
|
+
embedding ? {
|
|
2390
|
+
...message,
|
|
2391
|
+
embedding,
|
|
2392
|
+
embeddingModel: message.embeddingModel ?? deps.embeddingInfo?.model ?? "server",
|
|
2393
|
+
embeddingContentHash: sha256(message.content),
|
|
2394
|
+
embeddingDimensions: embedding.length,
|
|
2395
|
+
embeddingUpdatedAt: Date.now()
|
|
2396
|
+
} : message
|
|
2397
|
+
);
|
|
2398
|
+
}
|
|
2399
|
+
return out;
|
|
2400
|
+
}
|
|
2401
|
+
async function prepareRawMessageIngest(incoming, embedOnInsert, unified) {
|
|
2402
|
+
const deps = unified ?? {};
|
|
2403
|
+
const embedderAvailable = typeof deps.embedQuery === "function" || typeof deps.embedDocuments === "function";
|
|
2404
|
+
const shouldEmbed = embedOnInsert === true || embedderAvailable;
|
|
2405
|
+
const messages = [];
|
|
2406
|
+
const chunks = [];
|
|
2407
|
+
let missingSemanticEmbedding = false;
|
|
2408
|
+
let generatedEmbedding = false;
|
|
2409
|
+
const plans = incoming.map((message) => ({
|
|
2410
|
+
message,
|
|
2411
|
+
pieces: chunkTextByEstimatedTokens(message.content)
|
|
2412
|
+
}));
|
|
2413
|
+
const generatedEmbeddings = /* @__PURE__ */ new Map();
|
|
2414
|
+
const pendingByUser = /* @__PURE__ */ new Map();
|
|
2415
|
+
for (const [messageIndex, plan] of plans.entries()) {
|
|
2416
|
+
for (const piece of plan.pieces) {
|
|
2417
|
+
const canReuseParentEmbedding = plan.pieces.length === 1 && isUsableEmbedding(plan.message.embedding);
|
|
2418
|
+
if (canReuseParentEmbedding || !shouldEmbed || piece.content.length === 0) continue;
|
|
2419
|
+
const pending = pendingByUser.get(plan.message.userId) ?? [];
|
|
2420
|
+
pending.push({ key: `${messageIndex}:${piece.chunkIndex}`, text: piece.content });
|
|
2421
|
+
pendingByUser.set(plan.message.userId, pending);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
for (const [userId, pending] of pendingByUser) {
|
|
2425
|
+
let embeddings;
|
|
2426
|
+
if (typeof deps.embedDocuments === "function") {
|
|
2427
|
+
const batch = await deps.embedDocuments({
|
|
2428
|
+
userId,
|
|
2429
|
+
texts: pending.map((item) => item.text)
|
|
2430
|
+
});
|
|
2431
|
+
if (batch.length !== pending.length) {
|
|
2432
|
+
throw new Error(
|
|
2433
|
+
`Document embedding count mismatch: expected ${pending.length}, received ${batch.length}`
|
|
2434
|
+
);
|
|
2435
|
+
}
|
|
2436
|
+
embeddings = batch.map((embedding) => isUsableEmbedding(embedding) ? embedding : void 0);
|
|
2437
|
+
} else {
|
|
2438
|
+
embeddings = [];
|
|
2439
|
+
for (const item of pending) {
|
|
2440
|
+
embeddings.push(await embedText(item.text, userId, deps));
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
for (const [index, item] of pending.entries()) {
|
|
2444
|
+
const embedding = embeddings[index];
|
|
2445
|
+
if (embedding) generatedEmbeddings.set(item.key, embedding);
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
for (const [messageIndex, plan] of plans.entries()) {
|
|
2449
|
+
const incomingMessage = plan.message;
|
|
2450
|
+
const pieces = plan.pieces;
|
|
2451
|
+
let message = incomingMessage;
|
|
2452
|
+
for (const piece of pieces) {
|
|
2453
|
+
const contentHash = sha256(piece.content);
|
|
2454
|
+
const canReuseParentEmbedding = pieces.length === 1 && isUsableEmbedding(incomingMessage.embedding);
|
|
2455
|
+
const embedding = canReuseParentEmbedding ? incomingMessage.embedding : generatedEmbeddings.get(`${messageIndex}:${piece.chunkIndex}`);
|
|
2456
|
+
generatedEmbedding = generatedEmbedding || Boolean(embedding && !canReuseParentEmbedding);
|
|
2457
|
+
if (!embedding) missingSemanticEmbedding = true;
|
|
2458
|
+
const embeddingModel = embedding ? canReuseParentEmbedding ? incomingMessage.embeddingModel : deps.embeddingInfo?.model ?? incomingMessage.embeddingModel ?? "server" : void 0;
|
|
2459
|
+
chunks.push({
|
|
2460
|
+
chunkId: `${incomingMessage.messageId}:chunk:${piece.chunkIndex}:${contentHash.slice(0, 16)}`,
|
|
2461
|
+
messageId: incomingMessage.messageId,
|
|
2462
|
+
userId: incomingMessage.userId,
|
|
2463
|
+
chunkIndex: piece.chunkIndex,
|
|
2464
|
+
chunkCount: pieces.length,
|
|
2465
|
+
startPosition: piece.startPosition,
|
|
2466
|
+
endPosition: piece.endPosition,
|
|
2467
|
+
content: piece.content,
|
|
2468
|
+
contentHash,
|
|
2469
|
+
embedding,
|
|
2470
|
+
embeddingModel,
|
|
2471
|
+
embeddingDimensions: embedding?.length,
|
|
2472
|
+
embeddingUpdatedAt: embedding ? Date.now() : void 0
|
|
2473
|
+
});
|
|
2474
|
+
if (pieces.length === 1 && embedding && !incomingMessage.embedding?.length) {
|
|
2475
|
+
message = {
|
|
2476
|
+
...incomingMessage,
|
|
2477
|
+
embedding,
|
|
2478
|
+
embeddingModel,
|
|
2479
|
+
embeddingContentHash: contentHash,
|
|
2480
|
+
embeddingDimensions: embedding.length,
|
|
2481
|
+
embeddingUpdatedAt: Date.now()
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
messages.push(message);
|
|
2486
|
+
}
|
|
2487
|
+
const warnings = [];
|
|
2488
|
+
if (generatedEmbedding && embedOnInsert !== true) warnings.push(AUTO_APPLIED_WARNING);
|
|
2489
|
+
if (missingSemanticEmbedding) warnings.push(SEMANTIC_UNAVAILABLE_WARNING);
|
|
2490
|
+
return { messages, chunks, warnings };
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
// src/storage/raw-message-ingest.ts
|
|
2494
|
+
async function persistRawMessages(input) {
|
|
2495
|
+
const prepared = await prepareRawMessageIngest(input.messages, input.embedOnInsert, input.unified);
|
|
2496
|
+
let ids;
|
|
2497
|
+
if (typeof input.manager.storeMessagesWithSearchChunks === "function") {
|
|
2498
|
+
ids = await input.manager.storeMessagesWithSearchChunks(prepared.messages, prepared.chunks);
|
|
2499
|
+
} else {
|
|
2500
|
+
const legacyMessages = (input.embedOnInsert === true || typeof input.unified?.embedQuery === "function") && typeof input.unified?.embedQuery === "function" ? await embedMissingMessages(prepared.messages, input.unified) : prepared.messages;
|
|
2501
|
+
if (typeof input.manager.upsertRawMessages === "function") {
|
|
2502
|
+
await input.manager.upsertRawMessages({ userId: input.userId, messages: legacyMessages });
|
|
2503
|
+
} else if (typeof input.manager.storeMessages === "function") {
|
|
2504
|
+
ids = await input.manager.storeMessages(legacyMessages);
|
|
2505
|
+
} else {
|
|
2506
|
+
throw new Error("active raw-message manager exposes no supported write method");
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
if (input.externalIndex) {
|
|
2510
|
+
await input.externalIndex.replaceMessages(prepared.messages, prepared.chunks);
|
|
2511
|
+
}
|
|
2512
|
+
return { count: prepared.messages.length, ids, warnings: prepared.warnings };
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2175
2515
|
// src/http.ts
|
|
2176
|
-
init_chroma_memory_index();
|
|
2177
2516
|
init_raw_message_store();
|
|
2178
2517
|
init_sqlite_raw_message_store();
|
|
2179
2518
|
async function startHttpServer(options = {}) {
|
|
@@ -2203,7 +2542,18 @@ async function startHttpServer(options = {}) {
|
|
|
2203
2542
|
const vsaStorage = await getSQLiteVsaStore({ dbPath: vsaDbPath });
|
|
2204
2543
|
const vsa = createVsaRecall(vsaStorage);
|
|
2205
2544
|
const app = new Hono();
|
|
2206
|
-
app.get("/health", (c) =>
|
|
2545
|
+
app.get("/health", async (c) => {
|
|
2546
|
+
const retrieval = options.unified?.getRawMessageRetrievalStatus ? await options.unified.getRawMessageRetrievalStatus().catch(() => ({
|
|
2547
|
+
backend: "unknown",
|
|
2548
|
+
childCount: 0,
|
|
2549
|
+
embeddedChildCount: 0,
|
|
2550
|
+
indexedDimensions: [],
|
|
2551
|
+
semanticReady: false,
|
|
2552
|
+
lexicalReady: false,
|
|
2553
|
+
semanticDegradedReason: "health_check_failed"
|
|
2554
|
+
})) : void 0;
|
|
2555
|
+
return c.json({ ok: true, store: "memory", ts: Date.now(), retrieval });
|
|
2556
|
+
});
|
|
2207
2557
|
app.post("/v1/search", async (c) => {
|
|
2208
2558
|
const body = await c.req.json().catch(() => ({}));
|
|
2209
2559
|
const userId = typeof body.userId === "string" ? body.userId : null;
|
|
@@ -2227,6 +2577,8 @@ async function startHttpServer(options = {}) {
|
|
|
2227
2577
|
dateFrom: typeof body.dateFrom === "string" ? body.dateFrom : void 0,
|
|
2228
2578
|
dateTo: typeof body.dateTo === "string" ? body.dateTo : void 0,
|
|
2229
2579
|
reasoningStrategy: body.reasoningStrategy === "rewrite" || body.reasoningStrategy === "iterative" || body.reasoningStrategy === "union" ? body.reasoningStrategy : void 0,
|
|
2580
|
+
mergeStrategy: body.mergeStrategy === "rrf" || body.mergeStrategy === "similarity" ? body.mergeStrategy : void 0,
|
|
2581
|
+
includeRetrievalDiagnostics: body.includeRetrievalDiagnostics === true,
|
|
2230
2582
|
...wantsSynthesis ? {
|
|
2231
2583
|
synthesize: {
|
|
2232
2584
|
...responseSchema ? { responseSchema } : {}
|
|
@@ -2330,57 +2682,20 @@ async function startHttpServer(options = {}) {
|
|
|
2330
2682
|
...m,
|
|
2331
2683
|
userId: m.userId ?? body.userId
|
|
2332
2684
|
}));
|
|
2333
|
-
const
|
|
2334
|
-
|
|
2335
|
-
body.
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
messages
|
|
2343
|
-
});
|
|
2344
|
-
} else if (typeof manager3.storeMessages === "function") {
|
|
2345
|
-
const ids = await manager3.storeMessages(messages);
|
|
2346
|
-
result = { inserted: ids.length, ids };
|
|
2347
|
-
} else {
|
|
2348
|
-
return c.json(
|
|
2349
|
-
{
|
|
2350
|
-
error: "active raw-message manager exposes neither upsertRawMessages nor storeMessages"
|
|
2351
|
-
},
|
|
2352
|
-
500
|
|
2353
|
-
);
|
|
2354
|
-
}
|
|
2355
|
-
try {
|
|
2356
|
-
await upsertRawMessagesToChroma(messages);
|
|
2357
|
-
} catch (error) {
|
|
2358
|
-
console.warn("[memory-store/http] chroma upsert failed:", error);
|
|
2359
|
-
}
|
|
2360
|
-
try {
|
|
2361
|
-
const messagesWithEmbeddings = messages.filter(
|
|
2362
|
-
(m) => Array.isArray(m.embedding) && m.embedding.length > 0
|
|
2363
|
-
);
|
|
2364
|
-
if (messagesWithEmbeddings.length > 0) {
|
|
2365
|
-
for (const message of messagesWithEmbeddings) {
|
|
2366
|
-
if (typeof manager3.upsertVectorForMessage === "function") {
|
|
2367
|
-
manager3.upsertVectorForMessage(message.messageId, message.embedding);
|
|
2368
|
-
}
|
|
2369
|
-
}
|
|
2370
|
-
console.log(
|
|
2371
|
-
"[memory-store/http] Updated sqlite-vec vector table for",
|
|
2372
|
-
messagesWithEmbeddings.length,
|
|
2373
|
-
"message(s)"
|
|
2374
|
-
);
|
|
2375
|
-
}
|
|
2376
|
-
} catch (error) {
|
|
2377
|
-
console.warn("[memory-store/http] sqlite-vec vector update failed:", error);
|
|
2378
|
-
}
|
|
2685
|
+
const persisted = await persistRawMessages({
|
|
2686
|
+
manager: manager3,
|
|
2687
|
+
userId: body.userId,
|
|
2688
|
+
messages: incoming,
|
|
2689
|
+
embedOnInsert: body.embedOnInsert,
|
|
2690
|
+
unified: options.unified,
|
|
2691
|
+
externalIndex: options.unified?.rawMessageChildIndex
|
|
2692
|
+
});
|
|
2693
|
+
const result = persisted.ids ? { inserted: persisted.ids.length, ids: persisted.ids } : { inserted: persisted.count };
|
|
2379
2694
|
return c.json({
|
|
2380
2695
|
ok: true,
|
|
2381
|
-
count:
|
|
2696
|
+
count: persisted.count,
|
|
2382
2697
|
result,
|
|
2383
|
-
...warnings.length > 0 ? { warnings } : {}
|
|
2698
|
+
...persisted.warnings.length > 0 ? { warnings: persisted.warnings } : {}
|
|
2384
2699
|
});
|
|
2385
2700
|
});
|
|
2386
2701
|
app.get("/v1/raw-messages/:id", async (c) => {
|
|
@@ -2479,7 +2794,17 @@ async function startHttpServer(options = {}) {
|
|
|
2479
2794
|
});
|
|
2480
2795
|
const okfHttpSpecifier = "@melandlabs/okf/http";
|
|
2481
2796
|
const { registerOkfRoutes } = await import(okfHttpSpecifier);
|
|
2482
|
-
registerOkfRoutes(app, rawStore
|
|
2797
|
+
registerOkfRoutes(app, rawStore, {
|
|
2798
|
+
writeMessages: async (userId, messages) => {
|
|
2799
|
+
await persistRawMessages({
|
|
2800
|
+
manager: manager2,
|
|
2801
|
+
userId,
|
|
2802
|
+
messages,
|
|
2803
|
+
unified: options.unified,
|
|
2804
|
+
externalIndex: options.unified?.rawMessageChildIndex
|
|
2805
|
+
});
|
|
2806
|
+
}
|
|
2807
|
+
});
|
|
2483
2808
|
const server = serve({ fetch: app.fetch, port, hostname: host });
|
|
2484
2809
|
return {
|
|
2485
2810
|
url: `http://${host}:${port}`,
|
|
@@ -2831,6 +3156,121 @@ function createUserVoiceRewriter(options) {
|
|
|
2831
3156
|
};
|
|
2832
3157
|
}
|
|
2833
3158
|
|
|
3159
|
+
// src/storage/raw-message-child-vector-index.ts
|
|
3160
|
+
var RawMessageChildVectorIndex = class {
|
|
3161
|
+
backend;
|
|
3162
|
+
store;
|
|
3163
|
+
catalog;
|
|
3164
|
+
constructor(options) {
|
|
3165
|
+
this.backend = options.backend;
|
|
3166
|
+
this.store = options.store;
|
|
3167
|
+
this.catalog = options.catalog;
|
|
3168
|
+
}
|
|
3169
|
+
async replaceMessages(messages, chunks) {
|
|
3170
|
+
const chunksByMessage = /* @__PURE__ */ new Map();
|
|
3171
|
+
for (const chunk of chunks) {
|
|
3172
|
+
const group = chunksByMessage.get(chunk.messageId) ?? [];
|
|
3173
|
+
group.push(chunk);
|
|
3174
|
+
chunksByMessage.set(chunk.messageId, group);
|
|
3175
|
+
}
|
|
3176
|
+
for (const message of messages) {
|
|
3177
|
+
await this.store.deleteDocument(message.messageId);
|
|
3178
|
+
const vectorChunks = (chunksByMessage.get(message.messageId) ?? []).filter(
|
|
3179
|
+
(chunk) => Boolean(chunk.embedding?.length) && !chunk.embedding?.every((value) => value === 0)
|
|
3180
|
+
).map((chunk) => ({
|
|
3181
|
+
id: chunk.chunkId,
|
|
3182
|
+
documentId: message.messageId,
|
|
3183
|
+
content: chunk.content,
|
|
3184
|
+
embedding: chunk.embedding,
|
|
3185
|
+
metadata: {
|
|
3186
|
+
userId: message.userId,
|
|
3187
|
+
botId: message.botId,
|
|
3188
|
+
platform: message.platform,
|
|
3189
|
+
timestamp: message.timestamp,
|
|
3190
|
+
chunkIndex: chunk.chunkIndex,
|
|
3191
|
+
chunkCount: chunk.chunkCount,
|
|
3192
|
+
startPosition: chunk.startPosition,
|
|
3193
|
+
endPosition: chunk.endPosition,
|
|
3194
|
+
contentHash: chunk.contentHash,
|
|
3195
|
+
embeddingModel: chunk.embeddingModel
|
|
3196
|
+
}
|
|
3197
|
+
}));
|
|
3198
|
+
if (vectorChunks.length > 0) await this.store.addChunks(vectorChunks);
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
async search(input) {
|
|
3202
|
+
const results = input.hybrid && this.store.hybridSearch ? await this.store.hybridSearch({
|
|
3203
|
+
text: input.query ?? "",
|
|
3204
|
+
vector: input.queryEmbedding,
|
|
3205
|
+
limit: input.limit,
|
|
3206
|
+
candidateLimit: input.limit,
|
|
3207
|
+
fusion: "rrf",
|
|
3208
|
+
filter: { userId: input.userId }
|
|
3209
|
+
}) : await this.store.similaritySearch(input.queryEmbedding, input.limit, input.userId);
|
|
3210
|
+
return this.hydrate(results, input);
|
|
3211
|
+
}
|
|
3212
|
+
async getChunkCount() {
|
|
3213
|
+
return this.store.getChunkCount();
|
|
3214
|
+
}
|
|
3215
|
+
async hydrate(results, input) {
|
|
3216
|
+
const matchedChunks = await this.catalog.getRawMessageSearchChunks({
|
|
3217
|
+
chunkIds: results.map((result) => result.id),
|
|
3218
|
+
userId: input.userId
|
|
3219
|
+
});
|
|
3220
|
+
const matchedById = new Map(matchedChunks.map((chunk) => [chunk.chunkId, chunk]));
|
|
3221
|
+
const messageIds = [...new Set(matchedChunks.map((chunk) => chunk.messageId))];
|
|
3222
|
+
const [allChunks, parents] = await Promise.all([
|
|
3223
|
+
this.catalog.getRawMessageSearchChunks({ messageIds, userId: input.userId }),
|
|
3224
|
+
Promise.all(messageIds.map((messageId) => this.catalog.getMessageById(messageId)))
|
|
3225
|
+
]);
|
|
3226
|
+
const parentById = new Map(
|
|
3227
|
+
parents.filter((parent) => Boolean(parent)).map((parent) => [parent.messageId, parent])
|
|
3228
|
+
);
|
|
3229
|
+
const chunksByMessage = /* @__PURE__ */ new Map();
|
|
3230
|
+
for (const chunk of allChunks) {
|
|
3231
|
+
const group = chunksByMessage.get(chunk.messageId) ?? [];
|
|
3232
|
+
group.push(chunk);
|
|
3233
|
+
chunksByMessage.set(chunk.messageId, group);
|
|
3234
|
+
}
|
|
3235
|
+
const strongest = /* @__PURE__ */ new Map();
|
|
3236
|
+
for (const result of results) {
|
|
3237
|
+
const chunk = matchedById.get(result.id);
|
|
3238
|
+
if (!chunk) continue;
|
|
3239
|
+
const parent = parentById.get(chunk.messageId);
|
|
3240
|
+
if (!parent || input.botId && parent.botId !== input.botId) continue;
|
|
3241
|
+
if (input.threshold !== void 0 && result.score < input.threshold) continue;
|
|
3242
|
+
const siblings = (chunksByMessage.get(parent.messageId) ?? []).sort(
|
|
3243
|
+
(a, b) => a.chunkIndex - b.chunkIndex
|
|
3244
|
+
);
|
|
3245
|
+
const content = chunk.chunkCount <= 1 ? parent.content : parent.content.slice(
|
|
3246
|
+
siblings[Math.max(0, chunk.chunkIndex - 1)]?.startPosition ?? chunk.startPosition,
|
|
3247
|
+
siblings[Math.min(siblings.length - 1, chunk.chunkIndex + 1)]?.endPosition ?? chunk.endPosition
|
|
3248
|
+
);
|
|
3249
|
+
const hit = {
|
|
3250
|
+
id: parent.messageId,
|
|
3251
|
+
content,
|
|
3252
|
+
similarity: result.score,
|
|
3253
|
+
metadata: {
|
|
3254
|
+
...parent.metadata ?? {},
|
|
3255
|
+
userId: parent.userId,
|
|
3256
|
+
botId: parent.botId,
|
|
3257
|
+
platform: parent.platform,
|
|
3258
|
+
timestamp: parent.timestamp,
|
|
3259
|
+
sourceMessageId: parent.messageId,
|
|
3260
|
+
sourceChunkId: chunk.chunkId,
|
|
3261
|
+
sourceChunkIndex: chunk.chunkIndex,
|
|
3262
|
+
sourceChunkCount: chunk.chunkCount,
|
|
3263
|
+
backend: this.backend,
|
|
3264
|
+
scoring: input.hybrid ? "native-hybrid" : "dense"
|
|
3265
|
+
}
|
|
3266
|
+
};
|
|
3267
|
+
const existing = strongest.get(parent.messageId);
|
|
3268
|
+
if (!existing || hit.similarity > existing.similarity) strongest.set(parent.messageId, hit);
|
|
3269
|
+
}
|
|
3270
|
+
return [...strongest.values()].sort((a, b) => b.similarity - a.similarity).slice(0, input.limit);
|
|
3271
|
+
}
|
|
3272
|
+
};
|
|
3273
|
+
|
|
2834
3274
|
// src/server/cli-shared.ts
|
|
2835
3275
|
init_raw_message_store();
|
|
2836
3276
|
|
|
@@ -2845,7 +3285,7 @@ function isInsightSQLiteVecEnabled(_env) {
|
|
|
2845
3285
|
}
|
|
2846
3286
|
async function getInsightSQLiteVecStore(_env) {
|
|
2847
3287
|
const dbPath = resolveSQLiteRawMessageDbPath();
|
|
2848
|
-
const { getSQLiteVecStore } = await import('@melandlabs/rag/sqlite-vec-store');
|
|
3288
|
+
const { getSQLiteVecStore } = await import('@melandlabs/ai-rag/sqlite-vec-store');
|
|
2849
3289
|
return await getSQLiteVecStore(dbPath, void 0, {
|
|
2850
3290
|
collectionName: process.env.SQLITE_VEC_INSIGHTS_COLLECTION || "opencontext_insights"
|
|
2851
3291
|
});
|
|
@@ -2869,17 +3309,19 @@ async function searchInsightsWithSQLiteVec(input, env) {
|
|
|
2869
3309
|
// src/server/cli-shared.ts
|
|
2870
3310
|
async function loadAiRag() {
|
|
2871
3311
|
try {
|
|
2872
|
-
const [local, chroma] = await Promise.all([
|
|
3312
|
+
const [local, reranker, chroma] = await Promise.all([
|
|
2873
3313
|
import('@melandlabs/ai-rag/local-transformers-embedding-provider'),
|
|
3314
|
+
import('@melandlabs/ai-rag/local-transformers-reranker'),
|
|
2874
3315
|
import('@melandlabs/ai-rag/chroma-store')
|
|
2875
3316
|
]);
|
|
2876
3317
|
return {
|
|
2877
3318
|
LocalTransformersEmbeddingProvider: local.LocalTransformersEmbeddingProvider,
|
|
3319
|
+
LocalTransformersReranker: reranker.LocalTransformersReranker,
|
|
2878
3320
|
ChromaVectorStore: chroma.ChromaVectorStore
|
|
2879
3321
|
};
|
|
2880
3322
|
} catch (error) {
|
|
2881
3323
|
throw new Error(
|
|
2882
|
-
|
|
3324
|
+
`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})`
|
|
2883
3325
|
);
|
|
2884
3326
|
}
|
|
2885
3327
|
}
|
|
@@ -2888,8 +3330,21 @@ function parseUnifiedArgs(argv) {
|
|
|
2888
3330
|
const args = {
|
|
2889
3331
|
embeddingProvider: env.EMBEDDING_PROVIDER ?? "none",
|
|
2890
3332
|
embeddingModel: env.EMBEDDING_MODEL,
|
|
3333
|
+
embeddingCacheDir: env.LOCAL_EMBEDDING_CACHE_DIR,
|
|
3334
|
+
rerankerProvider: env.RERANKER_PROVIDER ?? "none",
|
|
3335
|
+
rerankerModel: env.LOCAL_RERANKER_MODEL,
|
|
3336
|
+
rerankerCacheDir: env.LOCAL_RERANKER_CACHE_DIR,
|
|
3337
|
+
rerankerBatchSize: env.LOCAL_RERANKER_BATCH_SIZE ? Number.parseInt(env.LOCAL_RERANKER_BATCH_SIZE, 10) : void 0,
|
|
3338
|
+
rerankerMaxTokens: env.LOCAL_RERANKER_MAX_TOKENS ? Number.parseInt(env.LOCAL_RERANKER_MAX_TOKENS, 10) : void 0,
|
|
2891
3339
|
chromaUrl: env.CHROMA_URL,
|
|
2892
3340
|
memoryBackend: env.MEMORY_BACKEND ?? "none",
|
|
3341
|
+
lancedbUri: env.LANCEDB_URI,
|
|
3342
|
+
lancedbTable: env.LANCEDB_TABLE,
|
|
3343
|
+
milvusAddress: env.MILVUS_ADDRESS,
|
|
3344
|
+
milvusToken: env.MILVUS_TOKEN,
|
|
3345
|
+
milvusDatabase: env.MILVUS_DATABASE,
|
|
3346
|
+
milvusCollection: env.MILVUS_COLLECTION,
|
|
3347
|
+
milvusDimension: env.MILVUS_DIMENSION ? Number.parseInt(env.MILVUS_DIMENSION, 10) : void 0,
|
|
2893
3348
|
insightsBackend: env.INSIGHTS_BACKEND ?? "none",
|
|
2894
3349
|
insightsCollection: env.INSIGHTS_COLLECTION ?? "opencontext_insights",
|
|
2895
3350
|
knowledgeBackend: env.KNOWLEDGE_BACKEND ?? "none",
|
|
@@ -2914,12 +3369,51 @@ function parseUnifiedArgs(argv) {
|
|
|
2914
3369
|
case "--embedding-model":
|
|
2915
3370
|
args.embeddingModel = takeValue();
|
|
2916
3371
|
break;
|
|
3372
|
+
case "--embedding-cache-dir":
|
|
3373
|
+
args.embeddingCacheDir = takeValue();
|
|
3374
|
+
break;
|
|
3375
|
+
case "--reranker-provider":
|
|
3376
|
+
args.rerankerProvider = takeValue();
|
|
3377
|
+
break;
|
|
3378
|
+
case "--reranker-model":
|
|
3379
|
+
args.rerankerModel = takeValue();
|
|
3380
|
+
break;
|
|
3381
|
+
case "--reranker-cache-dir":
|
|
3382
|
+
args.rerankerCacheDir = takeValue();
|
|
3383
|
+
break;
|
|
3384
|
+
case "--reranker-batch-size":
|
|
3385
|
+
args.rerankerBatchSize = Number.parseInt(takeValue(), 10);
|
|
3386
|
+
break;
|
|
3387
|
+
case "--reranker-max-tokens":
|
|
3388
|
+
args.rerankerMaxTokens = Number.parseInt(takeValue(), 10);
|
|
3389
|
+
break;
|
|
2917
3390
|
case "--chroma-url":
|
|
2918
3391
|
args.chromaUrl = takeValue();
|
|
2919
3392
|
break;
|
|
2920
3393
|
case "--memory-backend":
|
|
2921
3394
|
args.memoryBackend = takeValue();
|
|
2922
3395
|
break;
|
|
3396
|
+
case "--lancedb-uri":
|
|
3397
|
+
args.lancedbUri = takeValue();
|
|
3398
|
+
break;
|
|
3399
|
+
case "--lancedb-table":
|
|
3400
|
+
args.lancedbTable = takeValue();
|
|
3401
|
+
break;
|
|
3402
|
+
case "--milvus-address":
|
|
3403
|
+
args.milvusAddress = takeValue();
|
|
3404
|
+
break;
|
|
3405
|
+
case "--milvus-token":
|
|
3406
|
+
args.milvusToken = takeValue();
|
|
3407
|
+
break;
|
|
3408
|
+
case "--milvus-database":
|
|
3409
|
+
args.milvusDatabase = takeValue();
|
|
3410
|
+
break;
|
|
3411
|
+
case "--milvus-collection":
|
|
3412
|
+
args.milvusCollection = takeValue();
|
|
3413
|
+
break;
|
|
3414
|
+
case "--milvus-dimension":
|
|
3415
|
+
args.milvusDimension = Number.parseInt(takeValue(), 10);
|
|
3416
|
+
break;
|
|
2923
3417
|
case "--insights-backend":
|
|
2924
3418
|
args.insightsBackend = takeValue();
|
|
2925
3419
|
break;
|
|
@@ -2949,31 +3443,60 @@ function parseUnifiedArgs(argv) {
|
|
|
2949
3443
|
break;
|
|
2950
3444
|
}
|
|
2951
3445
|
}
|
|
2952
|
-
|
|
2953
|
-
["--embedding-provider", args.embeddingProvider],
|
|
2954
|
-
["--memory-backend", args.memoryBackend],
|
|
2955
|
-
["--insights-backend", args.insightsBackend],
|
|
2956
|
-
["--knowledge-backend", args.knowledgeBackend]
|
|
2957
|
-
]) {
|
|
2958
|
-
const allowed = ["local", "openrouter", "none", "chroma", "sqlite-vec"];
|
|
3446
|
+
const validate = (name, value, allowed) => {
|
|
2959
3447
|
if (!allowed.includes(value)) {
|
|
2960
3448
|
throw new Error(`[memory-store/cli] ${name} must be one of: ${allowed.join(", ")} (got "${value}")`);
|
|
2961
3449
|
}
|
|
3450
|
+
};
|
|
3451
|
+
validate("--embedding-provider", args.embeddingProvider, ["local", "openrouter", "none"]);
|
|
3452
|
+
validate("--reranker-provider", args.rerankerProvider, ["local", "none"]);
|
|
3453
|
+
validate("--memory-backend", args.memoryBackend, ["sqlite-vec", "chroma", "lancedb", "milvus", "none"]);
|
|
3454
|
+
validate("--insights-backend", args.insightsBackend, ["sqlite-vec", "chroma", "none"]);
|
|
3455
|
+
validate("--knowledge-backend", args.knowledgeBackend, ["chroma", "none"]);
|
|
3456
|
+
if (args.milvusDimension !== void 0 && (!Number.isInteger(args.milvusDimension) || args.milvusDimension <= 0)) {
|
|
3457
|
+
throw new Error("[memory-store/cli] --milvus-dimension must be a positive integer");
|
|
3458
|
+
}
|
|
3459
|
+
for (const [name, value] of [
|
|
3460
|
+
["--reranker-batch-size", args.rerankerBatchSize],
|
|
3461
|
+
["--reranker-max-tokens", args.rerankerMaxTokens]
|
|
3462
|
+
]) {
|
|
3463
|
+
if (value !== void 0 && (!Number.isInteger(value) || value <= 0)) {
|
|
3464
|
+
throw new Error(`[memory-store/cli] ${name} must be a positive integer`);
|
|
3465
|
+
}
|
|
2962
3466
|
}
|
|
2963
3467
|
return args;
|
|
2964
3468
|
}
|
|
2965
3469
|
async function buildUnified(args) {
|
|
2966
3470
|
const unified = {};
|
|
2967
3471
|
const log = (msg) => console.warn(`[memory-store/cli] ${msg}`);
|
|
2968
|
-
const wantsAiRag = args.embeddingProvider === "local" || args.
|
|
3472
|
+
const wantsAiRag = args.embeddingProvider === "local" || args.rerankerProvider === "local" || args.insightsBackend === "chroma" || args.knowledgeBackend === "chroma";
|
|
2969
3473
|
const aiRag = wantsAiRag ? await loadAiRag() : null;
|
|
2970
3474
|
if (args.embeddingProvider === "local") {
|
|
2971
3475
|
try {
|
|
2972
3476
|
if (!aiRag) {
|
|
2973
3477
|
throw new Error("ai-rag modules not loaded");
|
|
2974
3478
|
}
|
|
2975
|
-
const provider = new aiRag.LocalTransformersEmbeddingProvider({
|
|
2976
|
-
|
|
3479
|
+
const provider = new aiRag.LocalTransformersEmbeddingProvider({
|
|
3480
|
+
modelName: args.embeddingModel,
|
|
3481
|
+
cacheDir: args.embeddingCacheDir
|
|
3482
|
+
});
|
|
3483
|
+
unified.embeddingInfo = {
|
|
3484
|
+
provider: "local",
|
|
3485
|
+
model: args.embeddingModel ?? "Xenova/all-MiniLM-L6-v2",
|
|
3486
|
+
maxTokens: 512
|
|
3487
|
+
};
|
|
3488
|
+
unified.embedQuery = async ({ query }) => {
|
|
3489
|
+
const embedding = await provider.embedQuery(query);
|
|
3490
|
+
if (unified.embeddingInfo) unified.embeddingInfo.dimensions = embedding.length;
|
|
3491
|
+
return embedding;
|
|
3492
|
+
};
|
|
3493
|
+
unified.embedDocuments = async ({ texts }) => {
|
|
3494
|
+
const embeddings = await provider.embedDocuments(texts);
|
|
3495
|
+
if (unified.embeddingInfo && embeddings[0]) {
|
|
3496
|
+
unified.embeddingInfo.dimensions = embeddings[0].length;
|
|
3497
|
+
}
|
|
3498
|
+
return embeddings;
|
|
3499
|
+
};
|
|
2977
3500
|
log(
|
|
2978
3501
|
`embedQuery wired via LocalTransformersEmbeddingProvider (model=${args.embeddingModel ?? "Xenova/all-MiniLM-L6-v2"})`
|
|
2979
3502
|
);
|
|
@@ -2989,6 +3512,7 @@ async function buildUnified(args) {
|
|
|
2989
3512
|
}
|
|
2990
3513
|
const model = args.embeddingModel ?? "text-embedding-3-small";
|
|
2991
3514
|
const baseURL = "https://openrouter.ai/api/v1";
|
|
3515
|
+
unified.embeddingInfo = { provider: "openrouter", model };
|
|
2992
3516
|
unified.embedQuery = async ({ query }) => {
|
|
2993
3517
|
const res = await fetch(`${baseURL}/embeddings`, {
|
|
2994
3518
|
method: "POST",
|
|
@@ -3006,49 +3530,150 @@ async function buildUnified(args) {
|
|
|
3006
3530
|
const body = await res.json();
|
|
3007
3531
|
const embedding = body.data?.[0]?.embedding;
|
|
3008
3532
|
if (!embedding) throw new Error("openrouter embeddings response missing data[0].embedding");
|
|
3533
|
+
if (unified.embeddingInfo) unified.embeddingInfo.dimensions = embedding.length;
|
|
3009
3534
|
return embedding;
|
|
3010
3535
|
};
|
|
3011
3536
|
log(`embedQuery wired via openrouter (model=${model})`);
|
|
3012
3537
|
}
|
|
3013
|
-
if (args.
|
|
3014
|
-
|
|
3015
|
-
const
|
|
3016
|
-
|
|
3538
|
+
if (args.rerankerProvider === "local") {
|
|
3539
|
+
if (!aiRag) throw new Error("Local reranker modules were not loaded");
|
|
3540
|
+
const reranker = new aiRag.LocalTransformersReranker({
|
|
3541
|
+
modelName: args.rerankerModel,
|
|
3542
|
+
cacheDir: args.rerankerCacheDir,
|
|
3543
|
+
batchSize: args.rerankerBatchSize,
|
|
3544
|
+
maxTokens: args.rerankerMaxTokens
|
|
3545
|
+
});
|
|
3546
|
+
await reranker.warmup();
|
|
3547
|
+
unified.reranker = reranker;
|
|
3548
|
+
unified.rerankerInfo = {
|
|
3549
|
+
provider: "local",
|
|
3550
|
+
model: reranker.getModelName(),
|
|
3551
|
+
maxTokens: reranker.getMaxTokens(),
|
|
3552
|
+
ready: true
|
|
3553
|
+
};
|
|
3554
|
+
log(`reranker wired via local Transformers.js (model=${reranker.getModelName()})`);
|
|
3555
|
+
} else {
|
|
3556
|
+
unified.rerankerInfo = { provider: "none", ready: false };
|
|
3557
|
+
}
|
|
3558
|
+
if (args.memoryBackend !== "none") {
|
|
3559
|
+
if (args.memoryBackend === "chroma" && !args.chromaUrl) {
|
|
3560
|
+
throw new Error("--memory-backend=chroma requires --chroma-url <url> (or CHROMA_URL env)");
|
|
3561
|
+
}
|
|
3562
|
+
if (args.memoryBackend === "lancedb" && !args.lancedbUri) {
|
|
3563
|
+
throw new Error("--memory-backend=lancedb requires --lancedb-uri <uri> (or LANCEDB_URI env)");
|
|
3564
|
+
}
|
|
3565
|
+
if (args.memoryBackend === "milvus" && !args.milvusAddress) {
|
|
3566
|
+
throw new Error("--memory-backend=milvus requires --milvus-address <address> (or MILVUS_ADDRESS env)");
|
|
3567
|
+
}
|
|
3568
|
+
if (args.memoryBackend === "lancedb") {
|
|
3569
|
+
try {
|
|
3570
|
+
await import('@lancedb/lancedb');
|
|
3571
|
+
} catch (error) {
|
|
3572
|
+
throw new Error(
|
|
3573
|
+
`LanceDB requires the optional dependency @lancedb/lancedb (${error.message})`
|
|
3574
|
+
);
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
if (args.memoryBackend === "milvus") {
|
|
3578
|
+
try {
|
|
3579
|
+
await import('@zilliz/milvus2-sdk-node');
|
|
3580
|
+
} catch (error) {
|
|
3581
|
+
throw new Error(
|
|
3582
|
+
`Milvus requires the optional dependency @zilliz/milvus2-sdk-node (${error.message})`
|
|
3583
|
+
);
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
const rawStore = createRawMessageStore({ env: void 0 });
|
|
3587
|
+
const manager2 = await rawStore.getManager();
|
|
3588
|
+
if (typeof manager2.getRawMessageSearchChunks !== "function" || typeof manager2.getRawMessageSearchIndexStats !== "function") {
|
|
3017
3589
|
throw new Error(
|
|
3018
|
-
|
|
3590
|
+
`${args.memoryBackend} child retrieval requires the SQLite raw-message catalog; the Postgres manager remains on the legacy parent fallback in this release`
|
|
3019
3591
|
);
|
|
3020
3592
|
}
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3593
|
+
if (typeof manager2.lexicalSearchMessages === "function") {
|
|
3594
|
+
unified.searchRawMessagesLexical = async (input) => {
|
|
3595
|
+
const rows = await manager2.lexicalSearchMessages(input);
|
|
3596
|
+
return rows.map((row) => ({ ...row, metadata: row.metadata ?? {} }));
|
|
3597
|
+
};
|
|
3598
|
+
}
|
|
3599
|
+
let externalStore;
|
|
3600
|
+
if (args.memoryBackend === "chroma") {
|
|
3601
|
+
externalStore = new ChromaVectorStore({
|
|
3602
|
+
url: args.chromaUrl,
|
|
3603
|
+
collectionName: "opencontext_raw_message_chunks"
|
|
3604
|
+
});
|
|
3605
|
+
} else if (args.memoryBackend === "lancedb") {
|
|
3606
|
+
const { LanceDBStore } = await import('@melandlabs/rag/lancedb-store');
|
|
3607
|
+
externalStore = new LanceDBStore({
|
|
3608
|
+
uri: args.lancedbUri,
|
|
3609
|
+
tableName: args.lancedbTable ?? "opencontext_raw_message_chunks",
|
|
3610
|
+
defaultFusion: "rrf",
|
|
3611
|
+
candidateMultiplier: 4
|
|
3612
|
+
});
|
|
3613
|
+
} else if (args.memoryBackend === "milvus") {
|
|
3614
|
+
const { MilvusStore } = await import('@melandlabs/rag/milvus-store');
|
|
3615
|
+
externalStore = new MilvusStore({
|
|
3616
|
+
address: args.milvusAddress,
|
|
3617
|
+
token: args.milvusToken,
|
|
3618
|
+
database: args.milvusDatabase,
|
|
3619
|
+
collectionName: args.milvusCollection ?? "opencontext_raw_message_chunks",
|
|
3620
|
+
dimension: args.milvusDimension,
|
|
3621
|
+
defaultFusion: "rrf",
|
|
3622
|
+
candidateMultiplier: 4
|
|
3028
3623
|
});
|
|
3029
|
-
return rows.map((r) => ({
|
|
3030
|
-
id: r.id,
|
|
3031
|
-
content: r.content,
|
|
3032
|
-
similarity: r.similarity,
|
|
3033
|
-
metadata: r.metadata ?? {}
|
|
3034
|
-
}));
|
|
3035
|
-
};
|
|
3036
|
-
log("memory backend wired via sqlite-vec (uses the manager's searchMessagesSemantically)");
|
|
3037
|
-
} else if (args.memoryBackend === "chroma") {
|
|
3038
|
-
if (!args.chromaUrl)
|
|
3039
|
-
throw new Error("--memory-backend=chroma requires --chroma-url <url> (or CHROMA_URL env)");
|
|
3040
|
-
if (!aiRag) {
|
|
3041
|
-
throw new Error("ai-rag modules not loaded");
|
|
3042
3624
|
}
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3625
|
+
if (args.memoryBackend === "sqlite-vec") {
|
|
3626
|
+
if (typeof manager2.searchMessagesSemantically !== "function") {
|
|
3627
|
+
throw new Error("sqlite-vec child search is unavailable in the active raw-message manager");
|
|
3628
|
+
}
|
|
3629
|
+
unified.searchRawMessagesAnn = async ({ userId, queryEmbedding, limit, threshold, botId }) => {
|
|
3630
|
+
const rows = await manager2.searchMessagesSemantically({
|
|
3631
|
+
userId,
|
|
3632
|
+
queryEmbedding,
|
|
3633
|
+
limit,
|
|
3634
|
+
threshold,
|
|
3635
|
+
botId
|
|
3636
|
+
});
|
|
3637
|
+
return rows.map((row) => ({ ...row, metadata: row.metadata ?? {} }));
|
|
3638
|
+
};
|
|
3639
|
+
} else if (externalStore) {
|
|
3640
|
+
const childIndex = new RawMessageChildVectorIndex({
|
|
3641
|
+
backend: args.memoryBackend,
|
|
3642
|
+
store: externalStore,
|
|
3643
|
+
catalog: {
|
|
3644
|
+
getMessageById: (messageId) => manager2.getMessageById(messageId),
|
|
3645
|
+
// biome-ignore lint/style/noNonNullAssertion: guarded above
|
|
3646
|
+
getRawMessageSearchChunks: (input) => manager2.getRawMessageSearchChunks(input)
|
|
3647
|
+
}
|
|
3648
|
+
});
|
|
3649
|
+
unified.rawMessageChildIndex = childIndex;
|
|
3650
|
+
unified.searchRawMessagesAnn = (input) => childIndex.search(input);
|
|
3651
|
+
if (args.memoryBackend !== "chroma") {
|
|
3652
|
+
unified.searchRawMessagesHybrid = (input) => childIndex.search({ ...input, hybrid: true });
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
unified.getRawMessageRetrievalStatus = async () => {
|
|
3656
|
+
const stats = await manager2.getRawMessageSearchIndexStats();
|
|
3657
|
+
const embeddingDimensions = unified.embeddingInfo?.dimensions;
|
|
3658
|
+
const dimensionMismatch = embeddingDimensions !== void 0 && stats.embeddingDimensions.length > 0 && !stats.embeddingDimensions.includes(embeddingDimensions);
|
|
3659
|
+
const semanticReady = Boolean(unified.embedQuery) && (args.memoryBackend === "sqlite-vec" ? stats.semanticReady : stats.embeddedChunkCount > 0) && !dimensionMismatch;
|
|
3660
|
+
return {
|
|
3661
|
+
backend: args.memoryBackend,
|
|
3662
|
+
embeddingProvider: unified.embeddingInfo?.provider,
|
|
3663
|
+
embeddingModel: unified.embeddingInfo?.model,
|
|
3664
|
+
embeddingDimensions,
|
|
3665
|
+
childCount: stats.chunkCount,
|
|
3666
|
+
embeddedChildCount: stats.embeddedChunkCount,
|
|
3667
|
+
indexedDimensions: stats.embeddingDimensions,
|
|
3668
|
+
semanticReady,
|
|
3669
|
+
lexicalReady: stats.lexicalReady,
|
|
3670
|
+
semanticDegradedReason: dimensionMismatch ? "semantic_dimension_mismatch" : !unified.embedQuery ? "semantic_unavailable" : stats.embeddedChunkCount === 0 ? "semantic_child_vectors_empty" : void 0,
|
|
3671
|
+
rerankerProvider: unified.rerankerInfo?.provider,
|
|
3672
|
+
rerankerModel: unified.rerankerInfo?.model,
|
|
3673
|
+
rerankerReady: unified.rerankerInfo?.ready ?? false
|
|
3674
|
+
};
|
|
3050
3675
|
};
|
|
3051
|
-
log(
|
|
3676
|
+
log(`memory backend wired via ${args.memoryBackend} with SQLite child catalog and lexical search`);
|
|
3052
3677
|
}
|
|
3053
3678
|
if (args.insightsBackend === "sqlite-vec" && isInsightSQLiteVecEnabled()) {
|
|
3054
3679
|
if (!unified.embedQuery) {
|
|
@@ -3175,20 +3800,41 @@ function printUnifiedHelp() {
|
|
|
3175
3800
|
--embedding-model <name> Model name
|
|
3176
3801
|
(env: EMBEDDING_MODEL; local \u2192 Xenova/all-MiniLM-L6-v2,
|
|
3177
3802
|
openrouter \u2192 text-embedding-3-small)
|
|
3803
|
+
--embedding-cache-dir <path> Local embedding cache (env: LOCAL_EMBEDDING_CACHE_DIR)
|
|
3178
3804
|
Note: "local" dynamically imports @melandlabs/ai-rag (a peer install).
|
|
3179
3805
|
|
|
3806
|
+
Reranking (after RRF/source fusion, before final Top-K):
|
|
3807
|
+
--reranker-provider <name> local | none
|
|
3808
|
+
(env: RERANKER_PROVIDER, default: none)
|
|
3809
|
+
--reranker-model <name> Local sequence-classification model
|
|
3810
|
+
(env: LOCAL_RERANKER_MODEL,
|
|
3811
|
+
default: Xenova/ms-marco-MiniLM-L-6-v2)
|
|
3812
|
+
--reranker-cache-dir <path> Persistent model cache (env: LOCAL_RERANKER_CACHE_DIR)
|
|
3813
|
+
--reranker-batch-size <int> Pair scoring batch size (env: LOCAL_RERANKER_BATCH_SIZE,
|
|
3814
|
+
default: 8)
|
|
3815
|
+
--reranker-max-tokens <int> Query/document pair token limit
|
|
3816
|
+
(env: LOCAL_RERANKER_MAX_TOKENS, default: 512)
|
|
3817
|
+
|
|
3180
3818
|
Cross-source search (wires unified.searchKnowledge / searchInsights / searchRawMessagesAnn):
|
|
3181
3819
|
--chroma-url <url> Chroma server URL
|
|
3182
3820
|
(env: CHROMA_URL; required when any *-backend=chroma)
|
|
3183
|
-
--memory-backend <name> sqlite-vec | chroma | none
|
|
3821
|
+
--memory-backend <name> sqlite-vec | chroma | lancedb | milvus | none
|
|
3184
3822
|
(env: MEMORY_BACKEND, default: none)
|
|
3823
|
+
--lancedb-uri <uri> LanceDB directory or URI (env: LANCEDB_URI)
|
|
3824
|
+
--lancedb-table <name> Optional table name (env: LANCEDB_TABLE)
|
|
3825
|
+
--milvus-address <address> Milvus endpoint (env: MILVUS_ADDRESS)
|
|
3826
|
+
--milvus-token <token> Optional Milvus token (env: MILVUS_TOKEN)
|
|
3827
|
+
--milvus-database <name> Optional database (env: MILVUS_DATABASE)
|
|
3828
|
+
--milvus-collection <name> Optional collection (env: MILVUS_COLLECTION)
|
|
3829
|
+
--milvus-dimension <int> Optional vector dimension (env: MILVUS_DIMENSION)
|
|
3185
3830
|
--insights-backend <name> sqlite-vec | chroma | none
|
|
3186
3831
|
(env: INSIGHTS_BACKEND, default: none)
|
|
3187
3832
|
--insights-collection <name> Chroma collection (default: opencontext_insights)
|
|
3188
3833
|
--knowledge-backend <name> chroma | none
|
|
3189
3834
|
(env: KNOWLEDGE_BACKEND, default: none)
|
|
3190
3835
|
--knowledge-collection <name> Chroma collection (default: opencontext_knowledge)
|
|
3191
|
-
|
|
3836
|
+
Notes: memory Chroma uses @melandlabs/rag; insight/knowledge Chroma use
|
|
3837
|
+
@melandlabs/ai-rag. LanceDB and Milvus require their optional peer dependencies.
|
|
3192
3838
|
|
|
3193
3839
|
Reasoning (wires unified.reasoning.{queryRewriter, iterativePlanner}):
|
|
3194
3840
|
--reasoning Enable the LLM reasoning layer so /v1/search and
|