@memory-river/core 0.2.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/LICENSE +201 -0
- package/README.md +222 -0
- package/README.zh-TW.md +186 -0
- package/dist/api.d.ts +100 -0
- package/dist/api.js +156 -0
- package/dist/cognition/causal-attribution.d.ts +36 -0
- package/dist/cognition/causal-attribution.js +239 -0
- package/dist/cognition/causal-engine.d.ts +105 -0
- package/dist/cognition/causal-engine.js +150 -0
- package/dist/cognition/conflict-detector.d.ts +39 -0
- package/dist/cognition/conflict-detector.js +193 -0
- package/dist/cognition/global-working-memory.d.ts +53 -0
- package/dist/cognition/global-working-memory.js +211 -0
- package/dist/cognition/hooks-engine.d.ts +99 -0
- package/dist/cognition/hooks-engine.js +672 -0
- package/dist/cognition/ralph-core.d.ts +28 -0
- package/dist/cognition/ralph-core.js +104 -0
- package/dist/distill/concentrator-adapter.d.ts +167 -0
- package/dist/distill/concentrator-adapter.js +1876 -0
- package/dist/engine.d.ts +402 -0
- package/dist/engine.js +2254 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/lifecycle/cleanup-engine.d.ts +80 -0
- package/dist/lifecycle/cleanup-engine.js +162 -0
- package/dist/lifecycle/cleanup-state.d.ts +34 -0
- package/dist/lifecycle/cleanup-state.js +50 -0
- package/dist/lifecycle/night-consolidation.d.ts +102 -0
- package/dist/lifecycle/night-consolidation.js +640 -0
- package/dist/lifecycle/night-recovery.d.ts +40 -0
- package/dist/lifecycle/night-recovery.js +107 -0
- package/dist/paths.d.ts +17 -0
- package/dist/paths.js +16 -0
- package/dist/pipeline/capsule-bridge.d.ts +35 -0
- package/dist/pipeline/capsule-bridge.js +86 -0
- package/dist/pipeline/compact-request.d.ts +30 -0
- package/dist/pipeline/compact-request.js +66 -0
- package/dist/pipeline/inbox-watcher.d.ts +112 -0
- package/dist/pipeline/inbox-watcher.js +1039 -0
- package/dist/ports.d.ts +29 -0
- package/dist/ports.js +1 -0
- package/dist/providers/embedder-v5.d.ts +46 -0
- package/dist/providers/embedder-v5.js +155 -0
- package/dist/providers/ollama-embedding.d.ts +25 -0
- package/dist/providers/ollama-embedding.js +166 -0
- package/dist/retrieval/abstractness-judge.d.ts +14 -0
- package/dist/retrieval/abstractness-judge.js +87 -0
- package/dist/retrieval/coverage-selection.d.ts +3 -0
- package/dist/retrieval/coverage-selection.js +53 -0
- package/dist/retrieval/cross-encoder-gate.d.ts +40 -0
- package/dist/retrieval/cross-encoder-gate.js +239 -0
- package/dist/retrieval/retriever-v4.d.ts +78 -0
- package/dist/retrieval/retriever-v4.js +1200 -0
- package/dist/skills/validate.d.ts +6 -0
- package/dist/skills/validate.js +69 -0
- package/dist/storage.d.ts +19 -0
- package/dist/storage.js +54 -0
- package/dist/store/aux-table-maintenance.d.ts +5 -0
- package/dist/store/aux-table-maintenance.js +64 -0
- package/dist/store/graph-enumerator.d.ts +21 -0
- package/dist/store/graph-enumerator.js +185 -0
- package/dist/store/graph-store.d.ts +107 -0
- package/dist/store/graph-store.js +478 -0
- package/dist/store/status-manager.d.ts +44 -0
- package/dist/store/status-manager.js +235 -0
- package/dist/store/store-v4.d.ts +339 -0
- package/dist/store/store-v4.js +2871 -0
- package/dist/transcript/keyword-search.d.ts +9 -0
- package/dist/transcript/keyword-search.js +67 -0
- package/dist/transcript/rehydrate-keyword.d.ts +6 -0
- package/dist/transcript/rehydrate-keyword.js +29 -0
- package/dist/transcript/rehydrate.d.ts +33 -0
- package/dist/transcript/rehydrate.js +285 -0
- package/dist/transcript/transcript-archive.d.ts +46 -0
- package/dist/transcript/transcript-archive.js +516 -0
- package/dist/types.d.ts +409 -0
- package/dist/types.js +104 -0
- package/dist/util/bounded-map.d.ts +1 -0
- package/dist/util/bounded-map.js +8 -0
- package/dist/util/rate-limiter.d.ts +12 -0
- package/dist/util/rate-limiter.js +54 -0
- package/dist/util/session-identity.d.ts +65 -0
- package/dist/util/session-identity.js +227 -0
- package/dist/util/util-hash.d.ts +1 -0
- package/dist/util/util-hash.js +4 -0
- package/package.json +59 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const SKILL_NAME_RE = /^[\p{L}\p{N}_-]{1,50}$/u;
|
|
2
|
+
function charLength(value) {
|
|
3
|
+
return Array.from(value).length;
|
|
4
|
+
}
|
|
5
|
+
function received(value) {
|
|
6
|
+
return JSON.stringify(value) ?? String(value);
|
|
7
|
+
}
|
|
8
|
+
export class SkillValidationError extends Error {
|
|
9
|
+
violations;
|
|
10
|
+
constructor(violations) {
|
|
11
|
+
super(`skill_save rejected (${violations.length} violations):\n`
|
|
12
|
+
+ violations.map((violation, index) => `${index + 1}. ${violation}`).join('\n'));
|
|
13
|
+
this.violations = violations;
|
|
14
|
+
this.name = 'SkillValidationError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function validateSkillDef(def) {
|
|
18
|
+
const violations = [];
|
|
19
|
+
if (typeof def?.name !== 'string' || !SKILL_NAME_RE.test(def.name)) {
|
|
20
|
+
violations.push(`skillName: 不符 /^[\\p{L}\\p{N}_-]{1,50}$/u(收到 ${received(def?.name)})`);
|
|
21
|
+
}
|
|
22
|
+
if (typeof def?.summary !== 'string') {
|
|
23
|
+
violations.push(`summary: 需為字串(收到 ${received(def?.summary)})`);
|
|
24
|
+
}
|
|
25
|
+
else if (charLength(def.summary) < 1 || charLength(def.summary) > 200) {
|
|
26
|
+
violations.push(`summary: 需 1–200 chars(收到 ${charLength(def.summary)})`);
|
|
27
|
+
}
|
|
28
|
+
if (!Array.isArray(def?.triggers)) {
|
|
29
|
+
violations.push(`triggerConditions: 需 1–5 條(收到 ${received(def?.triggers)})`);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
if (def.triggers.length < 1 || def.triggers.length > 5) {
|
|
33
|
+
violations.push(`triggerConditions: 需 1–5 條(收到 ${def.triggers.length})`);
|
|
34
|
+
}
|
|
35
|
+
const seen = new Set();
|
|
36
|
+
def.triggers.forEach((trigger, index) => {
|
|
37
|
+
if (typeof trigger !== 'string') {
|
|
38
|
+
violations.push(`triggerConditions[${index}]: 需為字串(收到 ${received(trigger)})`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (charLength(trigger) > 100) {
|
|
42
|
+
violations.push(`triggerConditions[${index}]: 超過 100 chars(收到 ${charLength(trigger)})`);
|
|
43
|
+
}
|
|
44
|
+
if (seen.has(trigger)) {
|
|
45
|
+
violations.push(`triggerConditions[${index}]: 不得重複(收到 ${received(trigger)})`);
|
|
46
|
+
}
|
|
47
|
+
seen.add(trigger);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (!Array.isArray(def?.steps)) {
|
|
51
|
+
violations.push(`executionSteps: 需 2–15 步(收到 ${received(def?.steps)})`);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
if (def.steps.length < 2 || def.steps.length > 15) {
|
|
55
|
+
violations.push(`executionSteps: 需 2–15 步(收到 ${def.steps.length})`);
|
|
56
|
+
}
|
|
57
|
+
def.steps.forEach((step, index) => {
|
|
58
|
+
if (typeof step !== 'string') {
|
|
59
|
+
violations.push(`executionSteps[${index}]: 需為字串(收到 ${received(step)})`);
|
|
60
|
+
}
|
|
61
|
+
else if (charLength(step) > 300) {
|
|
62
|
+
violations.push(`executionSteps[${index}]: 超過 300 chars(收到 ${charLength(step)})`);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (violations.length > 0) {
|
|
67
|
+
throw new SkillValidationError(violations);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type StorageMode = 'auto' | 'ram' | 'ssd';
|
|
2
|
+
export declare const MIN_RAM_DB_BYTES: number;
|
|
3
|
+
export interface RamDbPathResolutionOptions {
|
|
4
|
+
dbPath: string;
|
|
5
|
+
ramDbPath: string;
|
|
6
|
+
storageMode?: StorageMode;
|
|
7
|
+
getShmFreeBytes?: () => number;
|
|
8
|
+
getDbSizeBytes?: (dbPath: string) => number;
|
|
9
|
+
log?: (message: string) => void;
|
|
10
|
+
}
|
|
11
|
+
export interface RamDbPathResolution {
|
|
12
|
+
ramDbPath: string;
|
|
13
|
+
mode: 'ram' | 'ssd-fallback';
|
|
14
|
+
requiredBytes?: number;
|
|
15
|
+
availableBytes?: number;
|
|
16
|
+
reason?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function getDevShmFreeBytes(): number;
|
|
19
|
+
export declare function resolveRamDbPath(options: RamDbPathResolutionOptions): RamDbPathResolution;
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
export const MIN_RAM_DB_BYTES = 512 * 1024 * 1024;
|
|
4
|
+
function directorySizeBytes(dir) {
|
|
5
|
+
try {
|
|
6
|
+
const metadata = fs.statSync(dir);
|
|
7
|
+
if (metadata.isFile())
|
|
8
|
+
return metadata.size;
|
|
9
|
+
let total = 0;
|
|
10
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
11
|
+
const entryPath = path.join(dir, entry.name);
|
|
12
|
+
if (entry.isDirectory())
|
|
13
|
+
total += directorySizeBytes(entryPath);
|
|
14
|
+
else if (entry.isFile())
|
|
15
|
+
total += fs.statSync(entryPath).size;
|
|
16
|
+
}
|
|
17
|
+
return total;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function getDevShmFreeBytes() {
|
|
24
|
+
try {
|
|
25
|
+
const stats = fs.statfsSync('/dev/shm');
|
|
26
|
+
return Number(stats.bavail) * Number(stats.bsize);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function resolveRamDbPath(options) {
|
|
33
|
+
const storageMode = options.storageMode ?? 'auto';
|
|
34
|
+
if (storageMode === 'ram')
|
|
35
|
+
return { ramDbPath: options.ramDbPath, mode: 'ram' };
|
|
36
|
+
if (storageMode === 'ssd') {
|
|
37
|
+
return { ramDbPath: options.dbPath, mode: 'ssd-fallback', reason: 'storageMode=ssd' };
|
|
38
|
+
}
|
|
39
|
+
const existingDbBytes = (options.getDbSizeBytes ?? directorySizeBytes)(options.dbPath);
|
|
40
|
+
const requiredBytes = Math.max(MIN_RAM_DB_BYTES, existingDbBytes * 2);
|
|
41
|
+
const availableBytes = (options.getShmFreeBytes ?? getDevShmFreeBytes)();
|
|
42
|
+
if (availableBytes >= requiredBytes) {
|
|
43
|
+
return { ramDbPath: options.ramDbPath, mode: 'ram', requiredBytes, availableBytes };
|
|
44
|
+
}
|
|
45
|
+
const reason = `/dev/shm has ${availableBytes} bytes free; ${requiredBytes} bytes required`;
|
|
46
|
+
(options.log ?? console.warn)(`[memory-river] RAM storage disabled: ${reason}. Using SSD fallback; set storageMode=ram to override.`);
|
|
47
|
+
return {
|
|
48
|
+
ramDbPath: options.dbPath,
|
|
49
|
+
mode: 'ssd-fallback',
|
|
50
|
+
requiredBytes,
|
|
51
|
+
availableBytes,
|
|
52
|
+
reason,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const AUX_TABLE_WRITE_MAINTENANCE_INTERVAL = 500;
|
|
2
|
+
export declare const AUX_TABLE_VERSION_RETENTION_MS: number;
|
|
3
|
+
export declare function optimizeAuxTable(table: any, label: string, cleanupAgeMs?: number): Promise<void>;
|
|
4
|
+
export declare function recordAuxTableWrite(table: any, label: string, cleanupAgeMs?: number): Promise<void>;
|
|
5
|
+
export declare function optimizeAuxTablesInConnection(db: any, label: string): Promise<void>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export const AUX_TABLE_WRITE_MAINTENANCE_INTERVAL = 500;
|
|
2
|
+
export const AUX_TABLE_VERSION_RETENTION_MS = 60 * 60 * 1000;
|
|
3
|
+
const writeCounts = new WeakMap();
|
|
4
|
+
const maintenanceInFlight = new WeakMap();
|
|
5
|
+
export async function optimizeAuxTable(table, label, cleanupAgeMs = AUX_TABLE_VERSION_RETENTION_MS) {
|
|
6
|
+
if (!table)
|
|
7
|
+
return;
|
|
8
|
+
const existing = maintenanceInFlight.get(table);
|
|
9
|
+
if (existing) {
|
|
10
|
+
await existing;
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const maintenance = (async () => {
|
|
14
|
+
try {
|
|
15
|
+
await table.optimize({
|
|
16
|
+
cleanupOlderThan: new Date(Date.now() - cleanupAgeMs),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
console.warn(`[AuxTableMaintenance] ${label} optimize failed (non-fatal):`, err?.message ?? err);
|
|
21
|
+
}
|
|
22
|
+
})();
|
|
23
|
+
maintenanceInFlight.set(table, maintenance);
|
|
24
|
+
try {
|
|
25
|
+
await maintenance;
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
maintenanceInFlight.delete(table);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function recordAuxTableWrite(table, label, cleanupAgeMs = AUX_TABLE_VERSION_RETENTION_MS) {
|
|
32
|
+
if (!table || (typeof table !== "object" && typeof table !== "function"))
|
|
33
|
+
return;
|
|
34
|
+
const next = (writeCounts.get(table) ?? 0) + 1;
|
|
35
|
+
if (next < AUX_TABLE_WRITE_MAINTENANCE_INTERVAL) {
|
|
36
|
+
writeCounts.set(table, next);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
writeCounts.set(table, 0);
|
|
40
|
+
await optimizeAuxTable(table, label, cleanupAgeMs);
|
|
41
|
+
}
|
|
42
|
+
export async function optimizeAuxTablesInConnection(db, label) {
|
|
43
|
+
if (!db)
|
|
44
|
+
return;
|
|
45
|
+
let tableNames;
|
|
46
|
+
try {
|
|
47
|
+
tableNames = await db.tableNames();
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
console.warn(`[AuxTableMaintenance] ${label} table listing failed (non-fatal):`, err?.message ?? err);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
for (const tableName of tableNames) {
|
|
54
|
+
if (tableName === "memories")
|
|
55
|
+
continue;
|
|
56
|
+
try {
|
|
57
|
+
const table = await db.openTable(tableName);
|
|
58
|
+
await optimizeAuxTable(table, `${label}:${tableName}`);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
console.warn(`[AuxTableMaintenance] ${label}:${tableName} maintenance failed (non-fatal):`, err?.message ?? err);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EnumerationPlan, EnumerationResult } from '../types.js';
|
|
2
|
+
import type { EmbeddingProvider } from '../ports.js';
|
|
3
|
+
import type { GraphStore } from './graph-store.js';
|
|
4
|
+
type GraphEnumeratorOptions = {
|
|
5
|
+
relationThreshold?: number;
|
|
6
|
+
fallbackPerRelationLimit?: number;
|
|
7
|
+
};
|
|
8
|
+
export declare class GraphEnumerator {
|
|
9
|
+
private readonly graphStore;
|
|
10
|
+
private readonly embedder;
|
|
11
|
+
readonly relationThreshold: number;
|
|
12
|
+
readonly fallbackPerRelationLimit: number;
|
|
13
|
+
constructor(graphStore: Pick<GraphStore, 'findTriplesByEntity' | 'findRelatedEntities'>, embedder: Pick<EmbeddingProvider, 'embed'>, options?: GraphEnumeratorOptions);
|
|
14
|
+
enumerate(plan: EnumerationPlan, limit?: number): Promise<EnumerationResult>;
|
|
15
|
+
private collectAnchorTriples;
|
|
16
|
+
private applyRelationFilter;
|
|
17
|
+
private fallbackTriples;
|
|
18
|
+
private projectAnswers;
|
|
19
|
+
private mergeAnswer;
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
function normalizeEntity(entity) {
|
|
2
|
+
return entity.trim().toLocaleLowerCase();
|
|
3
|
+
}
|
|
4
|
+
function cosineSimilarity(a, b) {
|
|
5
|
+
let dot = 0;
|
|
6
|
+
let aNorm = 0;
|
|
7
|
+
let bNorm = 0;
|
|
8
|
+
const length = Math.min(a.length, b.length);
|
|
9
|
+
for (let i = 0; i < length; i++) {
|
|
10
|
+
dot += a[i] * b[i];
|
|
11
|
+
aNorm += a[i] * a[i];
|
|
12
|
+
bNorm += b[i] * b[i];
|
|
13
|
+
}
|
|
14
|
+
if (aNorm === 0 || bNorm === 0)
|
|
15
|
+
return 0;
|
|
16
|
+
return dot / (Math.sqrt(aNorm) * Math.sqrt(bNorm));
|
|
17
|
+
}
|
|
18
|
+
function stableTripleCompare(a, b) {
|
|
19
|
+
return a.relation.localeCompare(b.relation)
|
|
20
|
+
|| a.object.localeCompare(b.object)
|
|
21
|
+
|| a.subject.localeCompare(b.subject)
|
|
22
|
+
|| a.id.localeCompare(b.id);
|
|
23
|
+
}
|
|
24
|
+
export class GraphEnumerator {
|
|
25
|
+
graphStore;
|
|
26
|
+
embedder;
|
|
27
|
+
relationThreshold;
|
|
28
|
+
fallbackPerRelationLimit;
|
|
29
|
+
constructor(graphStore, embedder, options = {}) {
|
|
30
|
+
this.graphStore = graphStore;
|
|
31
|
+
this.embedder = embedder;
|
|
32
|
+
const envThreshold = Number(process.env.MR_ENUM_RELATION_THRESHOLD);
|
|
33
|
+
this.relationThreshold = options.relationThreshold
|
|
34
|
+
?? (Number.isFinite(envThreshold) ? envThreshold : 0.5);
|
|
35
|
+
this.fallbackPerRelationLimit = options.fallbackPerRelationLimit ?? 50;
|
|
36
|
+
}
|
|
37
|
+
async enumerate(plan, limit = 1000) {
|
|
38
|
+
const anchors = [...new Set(plan.anchors.map(anchor => anchor.trim()).filter(Boolean))];
|
|
39
|
+
if (anchors.length === 0 || limit <= 0) {
|
|
40
|
+
return { answers: [], truncated: false, fallbackUsed: false };
|
|
41
|
+
}
|
|
42
|
+
const direction = plan.direction ?? 'both';
|
|
43
|
+
const perAnchor = new Map();
|
|
44
|
+
let fallbackUsed = false;
|
|
45
|
+
for (const anchor of anchors) {
|
|
46
|
+
const seededTriples = await this.collectAnchorTriples(anchor, direction, limit);
|
|
47
|
+
const filtered = await this.applyRelationFilter(seededTriples, plan.relationText);
|
|
48
|
+
fallbackUsed ||= filtered.fallbackUsed;
|
|
49
|
+
perAnchor.set(anchor, this.projectAnswers(direction, filtered.triples));
|
|
50
|
+
}
|
|
51
|
+
const requiredAnchors = anchors.length;
|
|
52
|
+
const merged = new Map();
|
|
53
|
+
for (const answers of perAnchor.values()) {
|
|
54
|
+
for (const [normalizedEntity, answer] of answers) {
|
|
55
|
+
const existing = merged.get(normalizedEntity);
|
|
56
|
+
if (!existing) {
|
|
57
|
+
merged.set(normalizedEntity, { ...answer, anchorCount: 1 });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
existing.anchorCount++;
|
|
61
|
+
this.mergeAnswer(existing, answer);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const keepAll = plan.setMode === 'union';
|
|
65
|
+
const answers = Array.from(merged.values())
|
|
66
|
+
.filter(answer => keepAll || answer.anchorCount === requiredAnchors)
|
|
67
|
+
.map(({ anchorCount: _anchorCount, ...answer }) => answer)
|
|
68
|
+
.sort((a, b) => a.entity.localeCompare(b.entity));
|
|
69
|
+
const truncated = answers.length > limit;
|
|
70
|
+
return {
|
|
71
|
+
answers: answers.slice(0, limit),
|
|
72
|
+
truncated,
|
|
73
|
+
fallbackUsed,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async collectAnchorTriples(anchor, direction, limit) {
|
|
77
|
+
const bySeedAndId = new Map();
|
|
78
|
+
const add = (seed, triples) => {
|
|
79
|
+
for (const triple of triples) {
|
|
80
|
+
bySeedAndId.set(`${normalizeEntity(seed)}\0${triple.id}`, { seed, triple });
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
add(anchor, await this.graphStore.findTriplesByEntity(anchor, direction, limit));
|
|
84
|
+
const related = await this.graphStore.findRelatedEntities(anchor);
|
|
85
|
+
const aliases = new Set();
|
|
86
|
+
for (const triple of related) {
|
|
87
|
+
if (triple.subject.trim())
|
|
88
|
+
aliases.add(triple.subject);
|
|
89
|
+
if (triple.object.trim())
|
|
90
|
+
aliases.add(triple.object);
|
|
91
|
+
}
|
|
92
|
+
for (const alias of aliases) {
|
|
93
|
+
add(alias, await this.graphStore.findTriplesByEntity(alias, direction, limit));
|
|
94
|
+
}
|
|
95
|
+
return Array.from(bySeedAndId.values());
|
|
96
|
+
}
|
|
97
|
+
async applyRelationFilter(seededTriples, relationText) {
|
|
98
|
+
const triples = seededTriples.map(item => item.triple);
|
|
99
|
+
if (triples.length === 0)
|
|
100
|
+
return { triples: [], fallbackUsed: false };
|
|
101
|
+
if (!relationText || relationText.trim() === '') {
|
|
102
|
+
return { triples: this.fallbackTriples(seededTriples), fallbackUsed: true };
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const queryVector = await this.embedder.embed(relationText, 'query');
|
|
106
|
+
const relationVectors = new Map();
|
|
107
|
+
const filtered = [];
|
|
108
|
+
for (const item of seededTriples) {
|
|
109
|
+
const triple = item.triple;
|
|
110
|
+
let relationVector = relationVectors.get(triple.relation);
|
|
111
|
+
if (!relationVector) {
|
|
112
|
+
relationVector = await this.embedder.embed(triple.relation, 'query');
|
|
113
|
+
relationVectors.set(triple.relation, relationVector);
|
|
114
|
+
}
|
|
115
|
+
if (cosineSimilarity(queryVector, relationVector) >= this.relationThreshold) {
|
|
116
|
+
filtered.push(item);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (filtered.length > 0)
|
|
120
|
+
return { triples: filtered, fallbackUsed: false };
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Fall through to deterministic fallback on embedding failures.
|
|
124
|
+
}
|
|
125
|
+
return { triples: this.fallbackTriples(seededTriples), fallbackUsed: true };
|
|
126
|
+
}
|
|
127
|
+
fallbackTriples(seededTriples) {
|
|
128
|
+
const byRelation = new Map();
|
|
129
|
+
for (const item of [...seededTriples].sort((a, b) => stableTripleCompare(a.triple, b.triple))) {
|
|
130
|
+
const triple = item.triple;
|
|
131
|
+
const group = byRelation.get(triple.relation) ?? [];
|
|
132
|
+
if (group.length < this.fallbackPerRelationLimit) {
|
|
133
|
+
group.push(item);
|
|
134
|
+
byRelation.set(triple.relation, group);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return Array.from(byRelation.keys())
|
|
138
|
+
.sort()
|
|
139
|
+
.flatMap(relation => byRelation.get(relation) ?? []);
|
|
140
|
+
}
|
|
141
|
+
projectAnswers(direction, seededTriples) {
|
|
142
|
+
const projected = [];
|
|
143
|
+
for (const item of seededTriples) {
|
|
144
|
+
const triple = item.triple;
|
|
145
|
+
const normalizedSeed = normalizeEntity(item.seed);
|
|
146
|
+
const subjectMatches = normalizeEntity(triple.subject) === normalizedSeed;
|
|
147
|
+
const objectMatches = normalizeEntity(triple.object) === normalizedSeed;
|
|
148
|
+
if ((direction === 'out' || direction === 'both') && subjectMatches) {
|
|
149
|
+
projected.push({ entity: triple.object, normalizedEntity: normalizeEntity(triple.object), triple });
|
|
150
|
+
}
|
|
151
|
+
if ((direction === 'in' || direction === 'both') && objectMatches) {
|
|
152
|
+
projected.push({ entity: triple.subject, normalizedEntity: normalizeEntity(triple.subject), triple });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const answers = new Map();
|
|
156
|
+
for (const item of projected) {
|
|
157
|
+
if (!item.normalizedEntity)
|
|
158
|
+
continue;
|
|
159
|
+
const answer = answers.get(item.normalizedEntity) ?? {
|
|
160
|
+
entity: item.entity,
|
|
161
|
+
evidenceTriples: [],
|
|
162
|
+
sourceMemoryIds: [],
|
|
163
|
+
};
|
|
164
|
+
if (!answer.evidenceTriples.some(triple => triple.id === item.triple.id)) {
|
|
165
|
+
answer.evidenceTriples.push(item.triple);
|
|
166
|
+
}
|
|
167
|
+
if (item.triple.sourceMemoryId && !answer.sourceMemoryIds.includes(item.triple.sourceMemoryId)) {
|
|
168
|
+
answer.sourceMemoryIds.push(item.triple.sourceMemoryId);
|
|
169
|
+
}
|
|
170
|
+
answers.set(item.normalizedEntity, answer);
|
|
171
|
+
}
|
|
172
|
+
return answers;
|
|
173
|
+
}
|
|
174
|
+
mergeAnswer(target, source) {
|
|
175
|
+
for (const triple of source.evidenceTriples) {
|
|
176
|
+
if (!target.evidenceTriples.some(existing => existing.id === triple.id)) {
|
|
177
|
+
target.evidenceTriples.push(triple);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const id of source.sourceMemoryIds) {
|
|
181
|
+
if (!target.sourceMemoryIds.includes(id))
|
|
182
|
+
target.sourceMemoryIds.push(id);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph Store Engine — 知識圖譜三元組儲存
|
|
3
|
+
* memory-river
|
|
4
|
+
*
|
|
5
|
+
* 在 LanceDB 中建立獨立的 `graph_triples` table,儲存實體關係三元組。
|
|
6
|
+
* 使用 ANN 向量搜尋支援圖譜語意查詢。
|
|
7
|
+
*
|
|
8
|
+
* 設計原則:
|
|
9
|
+
* - 與 MemoryStore 共用同一組 LanceDB 連線(SSD 持久化 + RAM 加速)
|
|
10
|
+
* - 寫入時同步 embed 三元組文字(subject + relation + object)
|
|
11
|
+
* - 查詢時用 findRelatedEntities 做 ANN 相似度搜尋,擴展 Hook trigger
|
|
12
|
+
*/
|
|
13
|
+
export interface Triple {
|
|
14
|
+
subject: string;
|
|
15
|
+
relation: string;
|
|
16
|
+
object: string;
|
|
17
|
+
}
|
|
18
|
+
export interface GraphTriple extends Triple {
|
|
19
|
+
id: string;
|
|
20
|
+
sourceMemoryId: string;
|
|
21
|
+
createdAt: number;
|
|
22
|
+
}
|
|
23
|
+
export declare class GraphStore {
|
|
24
|
+
private readonly ssdRecoveryProbeIntervalMs;
|
|
25
|
+
private ramDb;
|
|
26
|
+
private ramTable;
|
|
27
|
+
private ssdDb;
|
|
28
|
+
private ssdTable;
|
|
29
|
+
private initPromise;
|
|
30
|
+
private embedder;
|
|
31
|
+
private ssdAvailable;
|
|
32
|
+
private ssdConsecutiveFailures;
|
|
33
|
+
private ssdRecoveryProbeTimer;
|
|
34
|
+
private ssdRecoveryProbeInFlight;
|
|
35
|
+
private vectorDim;
|
|
36
|
+
/**
|
|
37
|
+
* @param ramDb RAM LanceDB 連接(由 MemoryStore 提供,透過 store.db)
|
|
38
|
+
* @param ssdDb SSD LanceDB 連接(由 MemoryStore 提供,透過 store.ssd)
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ Connections owned by MemoryStore — do not close.
|
|
41
|
+
* ⚠️ GraphStore MUST be constructed AFTER store.ensureInitialized().
|
|
42
|
+
*/
|
|
43
|
+
constructor(ramDb: any, ssdDb: any, embedder: {
|
|
44
|
+
embed(text: string): Promise<number[]>;
|
|
45
|
+
}, vectorDim?: number, ssdRecoveryProbeIntervalMs?: number);
|
|
46
|
+
private handleSsdSuccess;
|
|
47
|
+
private handleSsdError;
|
|
48
|
+
private startSsdRecoveryProbe;
|
|
49
|
+
private probeSsdRecovery;
|
|
50
|
+
private stopSsdRecoveryProbe;
|
|
51
|
+
private ensureInitialized;
|
|
52
|
+
private doInitialize;
|
|
53
|
+
private initTable;
|
|
54
|
+
/**
|
|
55
|
+
* 將三元組文字 Embedding 化(用於 ANN 搜尋)
|
|
56
|
+
* 拼接方式:subject + relation + object
|
|
57
|
+
*/
|
|
58
|
+
private embedTriple;
|
|
59
|
+
/**
|
|
60
|
+
* 寫入單筆三元組
|
|
61
|
+
*/
|
|
62
|
+
addTriple(triple: Triple, sourceMemoryId: string): Promise<GraphTriple>;
|
|
63
|
+
/**
|
|
64
|
+
* 批量寫入三元組
|
|
65
|
+
*/
|
|
66
|
+
addTriples(triples: Triple[], sourceMemoryId: string): Promise<GraphTriple[]>;
|
|
67
|
+
/**
|
|
68
|
+
* 用 Query 文字找相關實體(ANN 相似度搜尋)
|
|
69
|
+
* @param queryText 查詢文字
|
|
70
|
+
* @param limit 回傳上限
|
|
71
|
+
*/
|
|
72
|
+
findRelatedEntities(queryText: string, limit?: number): Promise<GraphTriple[]>;
|
|
73
|
+
/**
|
|
74
|
+
* 找某實體的所有三元組(subject 匹配)
|
|
75
|
+
*/
|
|
76
|
+
findTriplesBySubject(subject: string, limit?: number): Promise<GraphTriple[]>;
|
|
77
|
+
/**
|
|
78
|
+
* 找某實體的所有三元組(object 匹配)
|
|
79
|
+
*/
|
|
80
|
+
findTriplesByObject(object: string, limit?: number): Promise<GraphTriple[]>;
|
|
81
|
+
findTriplesByEntity(entity: string, direction?: 'out' | 'in' | 'both', limit?: number): Promise<GraphTriple[]>;
|
|
82
|
+
/**
|
|
83
|
+
* 查詢與某記憶 ID 關聯的所有三元組
|
|
84
|
+
*/
|
|
85
|
+
findTriplesByMemoryId(memoryId: string): Promise<GraphTriple[]>;
|
|
86
|
+
/**
|
|
87
|
+
* 圖譜語意擴展:給定 Query,回傳 subject/object 關鍵詞列表
|
|
88
|
+
* 用於 Hook trigger 時擴展 keyword matching 範圍
|
|
89
|
+
*
|
|
90
|
+
* @param queryText 原始 query
|
|
91
|
+
* @param limit 回傳數量上限
|
|
92
|
+
* @returns 擴展後的關鍵詞列表
|
|
93
|
+
*/
|
|
94
|
+
expandQueryKeywords(queryText: string, limit?: number): Promise<string[]>;
|
|
95
|
+
/**
|
|
96
|
+
* 圖譜語意擴展(完整版):回傳相關三元組 + 擴展後關鍵詞
|
|
97
|
+
* 供 Hook trigger 時使用
|
|
98
|
+
*/
|
|
99
|
+
semanticExpand(queryText: string, limit?: number): Promise<{
|
|
100
|
+
triples: GraphTriple[];
|
|
101
|
+
expandedKeywords: string[];
|
|
102
|
+
}>;
|
|
103
|
+
/** 取得目前圖譜大小(除錯用) */
|
|
104
|
+
count(): Promise<number>;
|
|
105
|
+
/** Graceful shutdown — connections owned by MemoryStore, do not close here */
|
|
106
|
+
shutdown(): Promise<void>;
|
|
107
|
+
}
|