atlas-mcp 0.1.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/.env.example +32 -0
- package/README.md +282 -0
- package/package.json +72 -0
- package/public/app/assets/app-CxbS1w9p.js +3981 -0
- package/public/app/assets/index-BA6nxCuI.css +1 -0
- package/public/app/assets/index-BXmIRrQH.js +177 -0
- package/public/app/index.html +27 -0
- package/public/assets/brain-atlas.LICENSE.txt +16 -0
- package/public/assets/brain-atlas.glb +0 -0
- package/public/assets/brain.obj +27282 -0
- package/public/fonts/DepartureMono-Regular.woff +0 -0
- package/public/fonts/DepartureMono-Regular.woff2 +0 -0
- package/scripts/sync-memory-vectors.js +46 -0
- package/src/audit.js +9 -0
- package/src/cli/args.js +87 -0
- package/src/cli/commands/add.js +103 -0
- package/src/cli/commands/config.js +228 -0
- package/src/cli/commands/delete.js +75 -0
- package/src/cli/commands/entities.js +39 -0
- package/src/cli/commands/entity.js +47 -0
- package/src/cli/commands/get.js +46 -0
- package/src/cli/commands/list.js +53 -0
- package/src/cli/commands/related.js +56 -0
- package/src/cli/commands/search.js +68 -0
- package/src/cli/commands/update.js +58 -0
- package/src/cli/deps.js +114 -0
- package/src/cli/env-file.js +44 -0
- package/src/cli/format.js +246 -0
- package/src/cli.js +187 -0
- package/src/cognitive-worker.js +381 -0
- package/src/db.js +2674 -0
- package/src/extraction-context.js +31 -0
- package/src/ingestion-service.js +387 -0
- package/src/ingestion-worker.js +225 -0
- package/src/llm-config.js +31 -0
- package/src/llm.js +789 -0
- package/src/logger.js +51 -0
- package/src/mcp-server.js +577 -0
- package/src/memory-comparison.js +421 -0
- package/src/related-memories.js +232 -0
- package/src/run-cognitive-worker.js +12 -0
- package/src/run-ingestion-worker.js +13 -0
- package/src/run-vector-worker.js +12 -0
- package/src/schemas.js +413 -0
- package/src/semantic-validation.js +430 -0
- package/src/server.js +827 -0
- package/src/shared/brain-regions.js +61 -0
- package/src/shared/entity-lens.js +249 -0
- package/src/shared/memory-placement.js +171 -0
- package/src/shared/memory-search.js +55 -0
- package/src/shared/region-anchors.js +112 -0
- package/src/shared/region-mapper.js +247 -0
- package/src/vector-store.js +546 -0
- package/src/vector-worker.js +71 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { getRegionContributions } from "./shared/region-mapper.js";
|
|
3
|
+
import { REGION_ANCHORS } from "./shared/region-anchors.js";
|
|
4
|
+
|
|
5
|
+
export function createMemoryComparisonInput(memory) {
|
|
6
|
+
const extraction = unwrapExtraction(memory.extraction);
|
|
7
|
+
return {
|
|
8
|
+
id: String(memory.id),
|
|
9
|
+
text: String(memory.raw_text || ""),
|
|
10
|
+
summary: String(memory.summary || extraction.summary || ""),
|
|
11
|
+
source: String(memory.source || "ui"),
|
|
12
|
+
createdAt: memory.created_at || null,
|
|
13
|
+
updatedAt: memory.updated_at || null,
|
|
14
|
+
ingestionDate: memory.ingestion_date || null,
|
|
15
|
+
occurredAt: normalizeOccurredAt(extraction.occurredAt),
|
|
16
|
+
types: normalizeWeightedValues(extraction.types, "type", "weight"),
|
|
17
|
+
entities: normalizeEntities(memory.entities, extraction.entities),
|
|
18
|
+
relationships: normalizeRelationships(
|
|
19
|
+
memory.relationships,
|
|
20
|
+
extraction.relationships,
|
|
21
|
+
),
|
|
22
|
+
actions: normalizeStrings(extraction.actions),
|
|
23
|
+
topics: normalizeStrings(extraction.topics),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function hashMemoryComparisonInput(leftMemory, rightMemory) {
|
|
28
|
+
const payload = {
|
|
29
|
+
left: createMemoryComparisonInput(leftMemory),
|
|
30
|
+
right: createMemoryComparisonInput(rightMemory),
|
|
31
|
+
};
|
|
32
|
+
return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildMemoryStructuralDiff(leftMemory, rightMemory) {
|
|
36
|
+
const left = createMemoryComparisonInput(leftMemory);
|
|
37
|
+
const right = createMemoryComparisonInput(rightMemory);
|
|
38
|
+
const leftExtraction = unwrapExtraction(leftMemory.extraction);
|
|
39
|
+
const rightExtraction = unwrapExtraction(rightMemory.extraction);
|
|
40
|
+
const regions = diffRegions(
|
|
41
|
+
leftMemory.regions,
|
|
42
|
+
rightMemory.regions,
|
|
43
|
+
leftExtraction,
|
|
44
|
+
rightExtraction,
|
|
45
|
+
);
|
|
46
|
+
return {
|
|
47
|
+
types: diffWeightedValues(left.types, right.types, "type"),
|
|
48
|
+
occurredAt: {
|
|
49
|
+
left: left.occurredAt,
|
|
50
|
+
right: right.occurredAt,
|
|
51
|
+
changed: JSON.stringify(left.occurredAt) !== JSON.stringify(right.occurredAt),
|
|
52
|
+
},
|
|
53
|
+
entities: diffValues(left.entities, right.entities, entityKey),
|
|
54
|
+
relationships: diffValues(
|
|
55
|
+
left.relationships,
|
|
56
|
+
right.relationships,
|
|
57
|
+
relationshipKey,
|
|
58
|
+
),
|
|
59
|
+
actions: diffValues(left.actions, right.actions),
|
|
60
|
+
topics: diffValues(left.topics, right.topics),
|
|
61
|
+
provenance: {
|
|
62
|
+
source: valueDelta(left.source, right.source),
|
|
63
|
+
createdAt: valueDelta(left.createdAt, right.createdAt),
|
|
64
|
+
updatedAt: valueDelta(left.updatedAt, right.updatedAt),
|
|
65
|
+
ingestionDate: valueDelta(left.ingestionDate, right.ingestionDate),
|
|
66
|
+
},
|
|
67
|
+
regions,
|
|
68
|
+
activationAnalysis: buildActivationAnalysis(
|
|
69
|
+
leftExtraction,
|
|
70
|
+
rightExtraction,
|
|
71
|
+
regions,
|
|
72
|
+
),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function unwrapExtraction(extraction) {
|
|
77
|
+
return extraction?.extraction_json || extraction || {};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeOccurredAt(value) {
|
|
81
|
+
return {
|
|
82
|
+
text: String(value?.text || ""),
|
|
83
|
+
normalized: value?.normalized || null,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeWeightedValues(values, labelKey, weightKey) {
|
|
88
|
+
return [...(values || [])]
|
|
89
|
+
.map((value) => ({
|
|
90
|
+
[labelKey]: String(value?.[labelKey] || "").trim(),
|
|
91
|
+
[weightKey]: finiteNumber(value?.[weightKey]),
|
|
92
|
+
}))
|
|
93
|
+
.filter((value) => value[labelKey])
|
|
94
|
+
.sort((a, b) => a[labelKey].localeCompare(b[labelKey]));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeEntities(storedEntities, extractedEntities) {
|
|
98
|
+
const values = storedEntities?.length
|
|
99
|
+
? storedEntities.map((entity) => ({
|
|
100
|
+
name: entity.canonical_name || entity.mention,
|
|
101
|
+
kind: entity.kind,
|
|
102
|
+
}))
|
|
103
|
+
: (extractedEntities || []).map((entity) => ({
|
|
104
|
+
name: entity.canonicalName || entity.mention,
|
|
105
|
+
kind: entity.kind,
|
|
106
|
+
}));
|
|
107
|
+
return uniqueBy(
|
|
108
|
+
values
|
|
109
|
+
.map((entity) => ({
|
|
110
|
+
name: String(entity.name || "").trim(),
|
|
111
|
+
kind: String(entity.kind || "").trim(),
|
|
112
|
+
}))
|
|
113
|
+
.filter((entity) => entity.name),
|
|
114
|
+
entityKey,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalizeRelationships(storedRelationships, extractedRelationships) {
|
|
119
|
+
const values = storedRelationships?.length
|
|
120
|
+
? storedRelationships.map((relationship) => ({
|
|
121
|
+
subject:
|
|
122
|
+
relationship.source_name ||
|
|
123
|
+
relationship.source?.canonical_name ||
|
|
124
|
+
relationship.subject,
|
|
125
|
+
predicate: relationship.predicate,
|
|
126
|
+
object:
|
|
127
|
+
relationship.target_name ||
|
|
128
|
+
relationship.target?.canonical_name ||
|
|
129
|
+
relationship.object,
|
|
130
|
+
}))
|
|
131
|
+
: extractedRelationships || [];
|
|
132
|
+
return uniqueBy(
|
|
133
|
+
values
|
|
134
|
+
.map((relationship) => ({
|
|
135
|
+
subject: String(relationship.subject || "").trim(),
|
|
136
|
+
predicate: String(relationship.predicate || "").trim(),
|
|
137
|
+
object: String(relationship.object || "").trim(),
|
|
138
|
+
}))
|
|
139
|
+
.filter(
|
|
140
|
+
(relationship) =>
|
|
141
|
+
relationship.subject ||
|
|
142
|
+
relationship.predicate ||
|
|
143
|
+
relationship.object,
|
|
144
|
+
),
|
|
145
|
+
relationshipKey,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function normalizeStrings(values) {
|
|
150
|
+
return [...new Set(
|
|
151
|
+
(values || [])
|
|
152
|
+
.map((value) => String(value || "").trim())
|
|
153
|
+
.filter(Boolean),
|
|
154
|
+
)].sort((a, b) => a.localeCompare(b));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function diffWeightedValues(left, right, key) {
|
|
158
|
+
const leftMap = new Map(left.map((value) => [normalizeKey(value[key]), value]));
|
|
159
|
+
const rightMap = new Map(right.map((value) => [normalizeKey(value[key]), value]));
|
|
160
|
+
const labels = [...new Set([...leftMap.keys(), ...rightMap.keys()])].sort();
|
|
161
|
+
return labels.map((label) => {
|
|
162
|
+
const leftValue = leftMap.get(label) || null;
|
|
163
|
+
const rightValue = rightMap.get(label) || null;
|
|
164
|
+
const leftWeight = leftValue?.weight ?? null;
|
|
165
|
+
const rightWeight = rightValue?.weight ?? null;
|
|
166
|
+
return {
|
|
167
|
+
type: leftValue?.[key] || rightValue?.[key] || label,
|
|
168
|
+
left: leftWeight,
|
|
169
|
+
right: rightWeight,
|
|
170
|
+
delta:
|
|
171
|
+
leftWeight === null || rightWeight === null
|
|
172
|
+
? null
|
|
173
|
+
: rightWeight - leftWeight,
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function diffValues(left, right, key = normalizeKey) {
|
|
179
|
+
const leftMap = new Map(left.map((value) => [key(value), value]));
|
|
180
|
+
const rightMap = new Map(right.map((value) => [key(value), value]));
|
|
181
|
+
const sharedKeys = [...leftMap.keys()]
|
|
182
|
+
.filter((value) => rightMap.has(value))
|
|
183
|
+
.sort();
|
|
184
|
+
return {
|
|
185
|
+
shared: sharedKeys.map((value) => leftMap.get(value)),
|
|
186
|
+
leftOnly: [...leftMap.keys()]
|
|
187
|
+
.filter((value) => !rightMap.has(value))
|
|
188
|
+
.sort()
|
|
189
|
+
.map((value) => leftMap.get(value)),
|
|
190
|
+
rightOnly: [...rightMap.keys()]
|
|
191
|
+
.filter((value) => !leftMap.has(value))
|
|
192
|
+
.sort()
|
|
193
|
+
.map((value) => rightMap.get(value)),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function diffRegions(
|
|
198
|
+
leftRegions = [],
|
|
199
|
+
rightRegions = [],
|
|
200
|
+
leftExtraction = {},
|
|
201
|
+
rightExtraction = {},
|
|
202
|
+
) {
|
|
203
|
+
const normalize = (regions) =>
|
|
204
|
+
new Map(
|
|
205
|
+
regions.map((region) => [
|
|
206
|
+
String(region.region),
|
|
207
|
+
{
|
|
208
|
+
weight: finiteNumber(region.weight),
|
|
209
|
+
left: nullableNumber(region.hemispheres?.left),
|
|
210
|
+
right: nullableNumber(region.hemispheres?.right),
|
|
211
|
+
},
|
|
212
|
+
]),
|
|
213
|
+
);
|
|
214
|
+
const left = normalize(leftRegions);
|
|
215
|
+
const right = normalize(rightRegions);
|
|
216
|
+
const leftContributions = groupRegionContributions(leftExtraction);
|
|
217
|
+
const rightContributions = groupRegionContributions(rightExtraction);
|
|
218
|
+
return [...new Set([...left.keys(), ...right.keys()])]
|
|
219
|
+
.sort()
|
|
220
|
+
.map((region) => {
|
|
221
|
+
const leftValue = left.get(region) || null;
|
|
222
|
+
const rightValue = right.get(region) || null;
|
|
223
|
+
return {
|
|
224
|
+
region,
|
|
225
|
+
label: REGION_ANCHORS[region]?.label || humanize(region),
|
|
226
|
+
role: REGION_ANCHORS[region]?.role || "",
|
|
227
|
+
left: leftValue,
|
|
228
|
+
right: rightValue,
|
|
229
|
+
leftReasons: describeContributions(leftContributions.get(region)),
|
|
230
|
+
rightReasons: describeContributions(rightContributions.get(region)),
|
|
231
|
+
delta: {
|
|
232
|
+
weight: subtractNullable(rightValue?.weight, leftValue?.weight),
|
|
233
|
+
leftHemisphere: subtractNullable(rightValue?.left, leftValue?.left),
|
|
234
|
+
rightHemisphere: subtractNullable(rightValue?.right, leftValue?.right),
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function buildActivationAnalysis(leftExtraction, rightExtraction, regions) {
|
|
241
|
+
const leftDominantType = getDominantType(leftExtraction);
|
|
242
|
+
const rightDominantType = getDominantType(rightExtraction);
|
|
243
|
+
const changedRegions = regions
|
|
244
|
+
.filter(({ delta }) => Math.abs(delta.weight || 0) >= 0.005)
|
|
245
|
+
.sort(
|
|
246
|
+
(a, b) =>
|
|
247
|
+
Math.abs(b.delta.weight) - Math.abs(a.delta.weight) ||
|
|
248
|
+
a.region.localeCompare(b.region),
|
|
249
|
+
);
|
|
250
|
+
const findings = changedRegions.map((region) => ({
|
|
251
|
+
region: region.region,
|
|
252
|
+
label: region.label,
|
|
253
|
+
role: region.role,
|
|
254
|
+
leftWeight: region.left?.weight ?? null,
|
|
255
|
+
rightWeight: region.right?.weight ?? null,
|
|
256
|
+
delta: region.delta.weight,
|
|
257
|
+
explanation: explainRegionDifference(region),
|
|
258
|
+
leftReasons: region.leftReasons,
|
|
259
|
+
rightReasons: region.rightReasons,
|
|
260
|
+
}));
|
|
261
|
+
const typeComparison =
|
|
262
|
+
leftDominantType && rightDominantType
|
|
263
|
+
? `The left memory is weighted most as ${leftDominantType.type} (${formatPercent(
|
|
264
|
+
leftDominantType.weight,
|
|
265
|
+
)}), while the right is weighted most as ${rightDominantType.type} (${formatPercent(
|
|
266
|
+
rightDominantType.weight,
|
|
267
|
+
)}).`
|
|
268
|
+
: "One or both memories lack a dominant extracted memory type.";
|
|
269
|
+
const largestShifts = changedRegions.slice(0, 3).map((region) => {
|
|
270
|
+
const side = region.delta.weight > 0 ? "right" : "left";
|
|
271
|
+
return `${region.label} is ${formatPercentagePoints(
|
|
272
|
+
Math.abs(region.delta.weight),
|
|
273
|
+
)} higher on the ${side}`;
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
summary: `${typeComparison}${
|
|
278
|
+
largestShifts.length
|
|
279
|
+
? ` The largest relative shifts are: ${largestShifts.join("; ")}.`
|
|
280
|
+
: " Their relative activation profiles are nearly the same."
|
|
281
|
+
}`,
|
|
282
|
+
findings,
|
|
283
|
+
notes: [
|
|
284
|
+
"Region percentages are normalized within each memory, so a stronger signal in one area can reduce another area's relative share even when its raw mapped signal is unchanged.",
|
|
285
|
+
"Emotion mapping uses extracted emotional type weight plus intensity and arousal. Positive or negative valence does not by itself select a different region.",
|
|
286
|
+
"These values explain Atlas's mapping heuristic; they are not measured neural activity or literal storage locations.",
|
|
287
|
+
],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function groupRegionContributions(extraction) {
|
|
292
|
+
const grouped = new Map();
|
|
293
|
+
for (const contribution of getRegionContributions(extraction)) {
|
|
294
|
+
const values = grouped.get(contribution.region) || [];
|
|
295
|
+
values.push(contribution);
|
|
296
|
+
grouped.set(contribution.region, values);
|
|
297
|
+
}
|
|
298
|
+
return grouped;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function describeContributions(contributions = []) {
|
|
302
|
+
return [...contributions]
|
|
303
|
+
.sort((a, b) => b.amount - a.amount)
|
|
304
|
+
.map((contribution) => {
|
|
305
|
+
if (contribution.source === "type") {
|
|
306
|
+
return `${capitalize(contribution.type)} type at ${formatPercent(
|
|
307
|
+
contribution.typeWeight,
|
|
308
|
+
)}, with ${formatPercent(
|
|
309
|
+
contribution.ruleWeight,
|
|
310
|
+
)} of that signal mapped here`;
|
|
311
|
+
}
|
|
312
|
+
if (contribution.source === "emotion") {
|
|
313
|
+
return `"${contribution.label}" at ${formatPercent(
|
|
314
|
+
contribution.intensity,
|
|
315
|
+
)} intensity and ${formatPercent(
|
|
316
|
+
contribution.arousal,
|
|
317
|
+
)} arousal, with ${formatPercent(
|
|
318
|
+
contribution.ruleWeight,
|
|
319
|
+
)} of that signal mapped here`;
|
|
320
|
+
}
|
|
321
|
+
return `Physical action "${contribution.action}" adds a motor signal`;
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function explainRegionDifference(region) {
|
|
326
|
+
const delta = region.delta.weight;
|
|
327
|
+
if (delta === null) {
|
|
328
|
+
const presentSide = region.left ? "left" : "right";
|
|
329
|
+
return `${region.label} is present only in the ${presentSide} memory's mapped profile.`;
|
|
330
|
+
}
|
|
331
|
+
if (Math.abs(delta) < 0.005) {
|
|
332
|
+
return `Both memories allocate nearly the same relative share to ${region.label}.`;
|
|
333
|
+
}
|
|
334
|
+
const higherSide = delta > 0 ? "right" : "left";
|
|
335
|
+
const higherReasons =
|
|
336
|
+
higherSide === "right" ? region.rightReasons : region.leftReasons;
|
|
337
|
+
const reason = higherReasons[0]
|
|
338
|
+
? ` The strongest mapped reason on that side is ${lowercaseFirst(
|
|
339
|
+
higherReasons[0],
|
|
340
|
+
)}.`
|
|
341
|
+
: "";
|
|
342
|
+
return `${region.label} is ${formatPercentagePoints(
|
|
343
|
+
Math.abs(delta),
|
|
344
|
+
)} higher in the ${higherSide} memory.${reason}`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function getDominantType(extraction) {
|
|
348
|
+
return [...(extraction.types || [])].sort(
|
|
349
|
+
(a, b) => b.weight - a.weight || a.type.localeCompare(b.type),
|
|
350
|
+
)[0] || null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function valueDelta(left, right) {
|
|
354
|
+
return { left, right, changed: left !== right };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function subtractNullable(right, left) {
|
|
358
|
+
return right === undefined ||
|
|
359
|
+
right === null ||
|
|
360
|
+
left === undefined ||
|
|
361
|
+
left === null
|
|
362
|
+
? null
|
|
363
|
+
: right - left;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function finiteNumber(value) {
|
|
367
|
+
const number = Number(value);
|
|
368
|
+
return Number.isFinite(number) ? number : 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function nullableNumber(value) {
|
|
372
|
+
if (value === undefined || value === null) return null;
|
|
373
|
+
const number = Number(value);
|
|
374
|
+
return Number.isFinite(number) ? number : null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function uniqueBy(values, key) {
|
|
378
|
+
return [...new Map(values.map((value) => [key(value), value])).values()].sort(
|
|
379
|
+
(a, b) => key(a).localeCompare(key(b)),
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function entityKey(entity) {
|
|
384
|
+
return `${normalizeKey(entity.kind)}:${normalizeKey(entity.name)}`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function relationshipKey(relationship) {
|
|
388
|
+
return [
|
|
389
|
+
relationship.subject,
|
|
390
|
+
relationship.predicate,
|
|
391
|
+
relationship.object,
|
|
392
|
+
].map(normalizeKey).join(":");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function normalizeKey(value) {
|
|
396
|
+
return String(value || "").trim().toLocaleLowerCase();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function formatPercent(value) {
|
|
400
|
+
return `${Math.round((Number(value) || 0) * 100)}%`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function formatPercentagePoints(value) {
|
|
404
|
+
return `${Math.round((Number(value) || 0) * 100)} percentage points`;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function humanize(value) {
|
|
408
|
+
return String(value || "")
|
|
409
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
410
|
+
.replace(/^./, (character) => character.toUpperCase());
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function capitalize(value) {
|
|
414
|
+
const text = String(value || "");
|
|
415
|
+
return text ? text[0].toUpperCase() + text.slice(1) : text;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function lowercaseFirst(value) {
|
|
419
|
+
const text = String(value || "");
|
|
420
|
+
return text ? text[0].toLowerCase() + text.slice(1) : text;
|
|
421
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { searchMemoriesFts } from "./db.js";
|
|
2
|
+
import { memoryEmbeddingText } from "./vector-store.js";
|
|
3
|
+
|
|
4
|
+
const ENTITY_SIGNAL_WEIGHTS = Object.freeze({
|
|
5
|
+
person: 0.55,
|
|
6
|
+
organization: 0.45,
|
|
7
|
+
place: 0.4,
|
|
8
|
+
concept: 0.3,
|
|
9
|
+
object: 0.25,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const RELATIONSHIP_SIGNAL_WEIGHT = 0.55;
|
|
13
|
+
const BM25_SIGNAL_WEIGHT = 0.3;
|
|
14
|
+
|
|
15
|
+
export async function getRelatedMemories(
|
|
16
|
+
memoryId,
|
|
17
|
+
{
|
|
18
|
+
getMemory,
|
|
19
|
+
getStructuralMemoryLinks,
|
|
20
|
+
searchMemoryVectors,
|
|
21
|
+
searchMemoriesFts: searchFts,
|
|
22
|
+
serializeMemory,
|
|
23
|
+
},
|
|
24
|
+
{ limit = 5, scoreThreshold = 0.65 } = {},
|
|
25
|
+
) {
|
|
26
|
+
const origin = getMemory(memoryId);
|
|
27
|
+
if (!origin) return null;
|
|
28
|
+
|
|
29
|
+
const structuralLinks = getStructuralMemoryLinks(memoryId) || [];
|
|
30
|
+
let semanticAvailable = true;
|
|
31
|
+
let semanticHits = [];
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
semanticHits = await searchMemoryVectors(memoryEmbeddingText(origin), {
|
|
35
|
+
limit: Math.max(limit * 4, 20),
|
|
36
|
+
scoreThreshold,
|
|
37
|
+
});
|
|
38
|
+
} catch {
|
|
39
|
+
semanticAvailable = false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let bm25Available = true;
|
|
43
|
+
let bm25Hits = [];
|
|
44
|
+
try {
|
|
45
|
+
const textForBm25 = [origin.title, origin.summary, origin.raw_text]
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.join(" ");
|
|
48
|
+
bm25Hits = searchFts
|
|
49
|
+
? searchFts(textForBm25, { limit: Math.max(limit * 4, 20) })
|
|
50
|
+
: searchMemoriesFts(textForBm25, { limit: Math.max(limit * 4, 20) });
|
|
51
|
+
} catch {
|
|
52
|
+
bm25Available = false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const candidates = new Map();
|
|
56
|
+
for (const link of structuralLinks) {
|
|
57
|
+
const candidateId = String(link.memory_id || link.memoryId || "");
|
|
58
|
+
if (!candidateId || candidateId === String(memoryId)) continue;
|
|
59
|
+
const candidate = ensureCandidate(candidates, candidateId);
|
|
60
|
+
candidate.sharedEntities = uniqueEntities(
|
|
61
|
+
link.shared_entities || link.sharedEntities || [],
|
|
62
|
+
);
|
|
63
|
+
candidate.sharedRelationships = uniqueRelationships(
|
|
64
|
+
link.shared_relationships || link.sharedRelationships || [],
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const hit of semanticHits) {
|
|
69
|
+
const candidateId = String(hit.id || "");
|
|
70
|
+
const similarity = Number(hit.score);
|
|
71
|
+
if (
|
|
72
|
+
!candidateId ||
|
|
73
|
+
candidateId === String(memoryId) ||
|
|
74
|
+
!Number.isFinite(similarity) ||
|
|
75
|
+
similarity < scoreThreshold
|
|
76
|
+
) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
ensureCandidate(candidates, candidateId).semanticSimilarity = similarity;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const maxBm25Score = Math.max(...bm25Hits.map((h) => h.score), 0);
|
|
83
|
+
for (const hit of bm25Hits) {
|
|
84
|
+
const candidateId = String(hit.id || "");
|
|
85
|
+
if (!candidateId || candidateId === String(memoryId)) continue;
|
|
86
|
+
const normalizedScore = maxBm25Score === 0
|
|
87
|
+
? 0
|
|
88
|
+
: Math.abs(hit.score) / Math.abs(maxBm25Score);
|
|
89
|
+
ensureCandidate(candidates, candidateId).bm25Score = normalizedScore;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const links = [...candidates.values()]
|
|
93
|
+
.flatMap((candidate) => {
|
|
94
|
+
const memory = getMemory(candidate.memoryId);
|
|
95
|
+
if (!memory) return [];
|
|
96
|
+
const signals = [
|
|
97
|
+
...candidate.sharedEntities.map(
|
|
98
|
+
(entity) => ENTITY_SIGNAL_WEIGHTS[entity.kind] || 0.2,
|
|
99
|
+
),
|
|
100
|
+
...candidate.sharedRelationships.map(
|
|
101
|
+
() => RELATIONSHIP_SIGNAL_WEIGHT,
|
|
102
|
+
),
|
|
103
|
+
];
|
|
104
|
+
if (candidate.semanticSimilarity != null) {
|
|
105
|
+
signals.push(clamp(candidate.semanticSimilarity, 0, 1));
|
|
106
|
+
}
|
|
107
|
+
if (candidate.bm25Score != null) {
|
|
108
|
+
signals.push(clamp(candidate.bm25Score, 0, 1) * BM25_SIGNAL_WEIGHT);
|
|
109
|
+
}
|
|
110
|
+
const score = combineSignals(signals);
|
|
111
|
+
if (score <= 0) return [];
|
|
112
|
+
|
|
113
|
+
return [{
|
|
114
|
+
memory: serializeMemory(memory),
|
|
115
|
+
score,
|
|
116
|
+
reasons: buildReasons(candidate),
|
|
117
|
+
sharedEntities: candidate.sharedEntities,
|
|
118
|
+
sharedRelationships: candidate.sharedRelationships,
|
|
119
|
+
semanticSimilarity: candidate.semanticSimilarity,
|
|
120
|
+
bm25Score: candidate.bm25Score,
|
|
121
|
+
}];
|
|
122
|
+
})
|
|
123
|
+
.sort(compareLinks)
|
|
124
|
+
.slice(0, limit);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
memoryId: String(memoryId),
|
|
128
|
+
links,
|
|
129
|
+
semanticAvailable,
|
|
130
|
+
bm25Available,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function combineSignals(signals) {
|
|
135
|
+
return 1 - signals.reduce(
|
|
136
|
+
(remaining, signal) => remaining * (1 - clamp(signal, 0, 1)),
|
|
137
|
+
1,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function ensureCandidate(candidates, memoryId) {
|
|
142
|
+
if (!candidates.has(memoryId)) {
|
|
143
|
+
candidates.set(memoryId, {
|
|
144
|
+
memoryId,
|
|
145
|
+
sharedEntities: [],
|
|
146
|
+
sharedRelationships: [],
|
|
147
|
+
semanticSimilarity: null,
|
|
148
|
+
bm25Score: null,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return candidates.get(memoryId);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function buildReasons(candidate) {
|
|
155
|
+
const reasons = candidate.sharedEntities.map(
|
|
156
|
+
(entity) => `Shared ${entity.kind}: ${entity.canonical_name}`,
|
|
157
|
+
);
|
|
158
|
+
reasons.push(
|
|
159
|
+
...candidate.sharedRelationships.map(
|
|
160
|
+
(relationship) =>
|
|
161
|
+
`Shared relationship: ${relationship.subject} `
|
|
162
|
+
+ `${relationship.predicate} ${relationship.object}`,
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
if (candidate.semanticSimilarity != null) {
|
|
166
|
+
reasons.push(
|
|
167
|
+
`Semantic similarity ${Math.round(candidate.semanticSimilarity * 100)}%`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (candidate.bm25Score != null) {
|
|
171
|
+
reasons.push(
|
|
172
|
+
`Keyword relevance ${Math.round(candidate.bm25Score * 100)}%`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return reasons;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function uniqueEntities(entities) {
|
|
179
|
+
const unique = new Map();
|
|
180
|
+
for (const entity of entities) {
|
|
181
|
+
const id = entity?.id ?? "";
|
|
182
|
+
const name = String(
|
|
183
|
+
entity?.canonical_name || entity?.canonicalName || entity?.name || "",
|
|
184
|
+
).trim();
|
|
185
|
+
const kind = String(entity?.kind || "concept").trim();
|
|
186
|
+
if (!name) continue;
|
|
187
|
+
const key = id ? `id:${id}` : `${kind}:${name.toLocaleLowerCase()}`;
|
|
188
|
+
if (!unique.has(key)) {
|
|
189
|
+
unique.set(key, {
|
|
190
|
+
...(id !== "" ? { id } : {}),
|
|
191
|
+
canonical_name: name,
|
|
192
|
+
kind,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return [...unique.values()];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function uniqueRelationships(relationships) {
|
|
200
|
+
const unique = new Map();
|
|
201
|
+
for (const relationship of relationships) {
|
|
202
|
+
const subject = String(
|
|
203
|
+
relationship?.subject
|
|
204
|
+
|| relationship?.source_name
|
|
205
|
+
|| relationship?.source?.canonical_name
|
|
206
|
+
|| "",
|
|
207
|
+
).trim();
|
|
208
|
+
const predicate = String(relationship?.predicate || "").trim();
|
|
209
|
+
const object = String(
|
|
210
|
+
relationship?.object
|
|
211
|
+
|| relationship?.target_name
|
|
212
|
+
|| relationship?.target?.canonical_name
|
|
213
|
+
|| "",
|
|
214
|
+
).trim();
|
|
215
|
+
if (!subject || !predicate || !object) continue;
|
|
216
|
+
const key = `${subject}\u0000${predicate}\u0000${object}`.toLocaleLowerCase();
|
|
217
|
+
if (!unique.has(key)) unique.set(key, { subject, predicate, object });
|
|
218
|
+
}
|
|
219
|
+
return [...unique.values()];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function compareLinks(left, right) {
|
|
223
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
224
|
+
const rightCreated = Date.parse(right.memory.created_at || "") || 0;
|
|
225
|
+
const leftCreated = Date.parse(left.memory.created_at || "") || 0;
|
|
226
|
+
if (rightCreated !== leftCreated) return rightCreated - leftCreated;
|
|
227
|
+
return String(left.memory.id).localeCompare(String(right.memory.id));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function clamp(value, minimum, maximum) {
|
|
231
|
+
return Math.min(Math.max(Number(value) || 0, minimum), maximum);
|
|
232
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import * as db from "./db.js";
|
|
4
|
+
import { annotateMemory } from "./llm.js";
|
|
5
|
+
import { createCognitiveWorker } from "./cognitive-worker.js";
|
|
6
|
+
|
|
7
|
+
const worker = createCognitiveWorker({ db, annotateMemory });
|
|
8
|
+
const controller = new AbortController();
|
|
9
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
10
|
+
process.once(signal, () => controller.abort());
|
|
11
|
+
}
|
|
12
|
+
await worker.run({ signal: controller.signal });
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import * as db from "./db.js";
|
|
4
|
+
import { defaultDependencies } from "./cli/deps.js";
|
|
5
|
+
import { createIngestionWorker } from "./ingestion-worker.js";
|
|
6
|
+
|
|
7
|
+
const { ingestionService } = defaultDependencies();
|
|
8
|
+
const worker = createIngestionWorker({ db, ingestionService });
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
11
|
+
process.once(signal, () => controller.abort());
|
|
12
|
+
}
|
|
13
|
+
await worker.run({ signal: controller.signal });
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import * as db from "./db.js";
|
|
4
|
+
import { indexMemoryVector } from "./vector-store.js";
|
|
5
|
+
import { createVectorWorker } from "./vector-worker.js";
|
|
6
|
+
|
|
7
|
+
const worker = createVectorWorker({ db, indexMemoryVector });
|
|
8
|
+
const controller = new AbortController();
|
|
9
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
10
|
+
process.once(signal, () => controller.abort());
|
|
11
|
+
}
|
|
12
|
+
await worker.run({ signal: controller.signal });
|