recallnest 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/CLAUDE.md +29 -0
- package/.claude-plugin/marketplace.json +18 -0
- package/.env.example +3 -0
- package/.github/workflows/ci.yml +28 -0
- package/.mcp.json +9 -0
- package/CHANGELOG.md +51 -0
- package/CLAUDE.md +42 -0
- package/DEMO.md +94 -0
- package/EVAL.md +89 -0
- package/FAILURES.md +75 -0
- package/LICENSE +21 -0
- package/OPERATIONS.md +59 -0
- package/README.md +427 -0
- package/README_CN.md +425 -0
- package/ROADMAP.md +198 -0
- package/assets/recallnest-workbench.png +0 -0
- package/assets/ui/app.js +807 -0
- package/assets/ui/index.html +154 -0
- package/assets/ui/styles.css +605 -0
- package/assets/wechat-qr.jpg +0 -0
- package/config.json.example +38 -0
- package/daily-recall.sh +87 -0
- package/daily-reflections/2026-03-05.md +15 -0
- package/daily-reflections/2026-03-06.md +15 -0
- package/daily-reflections/2026-03-07.md +7 -0
- package/daily-reflections/2026-03-08.md +34 -0
- package/daily-reflections/2026-03-09.md +35 -0
- package/daily-reflections/2026-03-10.md +36 -0
- package/daily-reflections/2026-03-11.md +35 -0
- package/daily-reflections/2026-03-12.md +36 -0
- package/daily-reflections/2026-03-13.md +35 -0
- package/daily-reflections/2026-03-14.md +35 -0
- package/daily-reflections/2026-03-15.md +35 -0
- package/daily-reflections/2026-03-16.md +41 -0
- package/daily-reflections/2026-03-17.md +10 -0
- package/daily-reflections/2026-03-18.md +10 -0
- package/daily-reflections/2026-03-19.md +10 -0
- package/daily-reflections/2026-03-20.md +10 -0
- package/daily-reflections/2026-03-21.md +10 -0
- package/daily-reflections/2026-03-22.md +10 -0
- package/daily-reflections/2026-03-23.md +10 -0
- package/daily-reflections/2026-03-24.md +10 -0
- package/daily-reflections/2026-03-25.md +32 -0
- package/daily-reflections/2026-03-26.md +37 -0
- package/daily-reflections/2026-03-27.md +35 -0
- package/daily-reflections/2026-03-28.md +35 -0
- package/daily-reflections/2026-03-29.md +35 -0
- package/daily-reflections/2026-03-30.md +36 -0
- package/daily-reflections/2026-03-31.md +35 -0
- package/daily-reflections/2026-04-01.md +35 -0
- package/docs/api-reference.md +1291 -0
- package/docs/architecture.md +168 -0
- package/docs/memory-boundary-contract.md +226 -0
- package/docs/memory-categories.md +119 -0
- package/docs/pipeline-audit-ultramemory.md +177 -0
- package/docs/plans/2026-03-08-query-expansion.md +270 -0
- package/docs/plans/2026-03-16-continuity-layer.md +196 -0
- package/docs/plans/mcp-tiering-plan.md +126 -0
- package/docs/research/recall-quality-audit-2026-03-22.md +153 -0
- package/docs/self-evolution.md +198 -0
- package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +782 -0
- package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +77 -0
- package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +95 -0
- package/eval/cases.json +85 -0
- package/eval/continuity/case-seeds.json +90 -0
- package/eval/continuity/cases.json +302 -0
- package/eval/continuity/memory-seeds.json +26 -0
- package/eval/continuity/pattern-seeds.json +100 -0
- package/eval/continuity/results-2026-03-16.md +117 -0
- package/eval/continuity/results-2026-03-19.md +453 -0
- package/eval/continuity/runbook.md +110 -0
- package/eval/continuity/seed-cases.md +54 -0
- package/eval/reports/2026-03-06-abstract-query-baseline.md +96 -0
- package/eval/reports/2026-03-06-aws-ssh-baseline.md +82 -0
- package/eval/reports/2026-03-06-baseline.md +82 -0
- package/eval/reports/query-expansion.md +138 -0
- package/integrations/README.md +57 -0
- package/integrations/claude-code/README.md +133 -0
- package/integrations/claude-code/claude-md-snippet.md +31 -0
- package/integrations/claude-code/setup.sh +66 -0
- package/integrations/claude-code/smoke-continuity.sh +452 -0
- package/integrations/codex/README.md +49 -0
- package/integrations/codex/agents-md-snippet.md +31 -0
- package/integrations/codex/setup.sh +58 -0
- package/integrations/examples/claude-agent-sdk/memory-agent.ts +182 -0
- package/integrations/examples/langchain/memory-chain.py +111 -0
- package/integrations/examples/openai-agents-sdk/memory-agent.py +109 -0
- package/integrations/gemini-cli/README.md +57 -0
- package/integrations/gemini-cli/gemini-md-snippet.md +31 -0
- package/integrations/gemini-cli/setup.sh +64 -0
- package/integrations/shared/install-managed-block.sh +51 -0
- package/lm +19 -0
- package/package.json +50 -0
- package/plan-v1.2-distributable.md +105 -0
- package/scripts/archive-distilled-junk.ts +116 -0
- package/scripts/backfill-l0l1l2.ts +98 -0
- package/scripts/blind-scan.ts +588 -0
- package/scripts/cleanup-step1-exact-dups.ts +117 -0
- package/scripts/cleanup-step2-template-dups.ts +113 -0
- package/scripts/cleanup-step3-stale-content.ts +149 -0
- package/scripts/cleanup-step4-orphan-scopes.ts +131 -0
- package/scripts/delete-distilled-junk.ts +88 -0
- package/scripts/distill-facts.ts +305 -0
- package/scripts/health-check.ts +343 -0
- package/scripts/incremental-ingest.sh +71 -0
- package/scripts/mlp-health-check.ts +382 -0
- package/scripts/pull-claude-desktop.ts +515 -0
- package/scripts/re-ingest-cc.sh +66 -0
- package/scripts/rebalance-memory-health.ts +181 -0
- package/scripts/run-desktop-ingest.sh +11 -0
- package/scripts/start-server.sh +12 -0
- package/scripts/viz-dashboard.html +423 -0
- package/scripts/viz-export.ts +703 -0
- package/src/__tests__/access-tracker-novelty.test.ts +228 -0
- package/src/__tests__/auto-consolidation.test.ts +168 -0
- package/src/__tests__/auto-recall.test.ts +131 -0
- package/src/__tests__/batch-dedup.test.ts +65 -0
- package/src/__tests__/capture-engine.test.ts +1142 -0
- package/src/__tests__/capture-heuristic.test.ts +171 -0
- package/src/__tests__/cascade-forget.test.ts +210 -0
- package/src/__tests__/category-min-scores.test.ts +33 -0
- package/src/__tests__/confidence-tracker.test.ts +196 -0
- package/src/__tests__/conflict-advisor.test.ts +206 -0
- package/src/__tests__/conflict-escalation.test.ts +149 -0
- package/src/__tests__/conflict-lifecycle.test.ts +83 -0
- package/src/__tests__/conflict-output.test.ts +85 -0
- package/src/__tests__/conflict-store.test.ts +374 -0
- package/src/__tests__/consolidation-engine.test.ts +181 -0
- package/src/__tests__/context-composer-pins.test.ts +64 -0
- package/src/__tests__/context-composer-task-selection.test.ts +24 -0
- package/src/__tests__/context-renderer.test.ts +91 -0
- package/src/__tests__/continuity-eval.test.ts +275 -0
- package/src/__tests__/continuity-schema.test.ts +97 -0
- package/src/__tests__/core-recall-eval.test.ts +235 -0
- package/src/__tests__/core-summary.test.ts +109 -0
- package/src/__tests__/dedup-actions.test.ts +58 -0
- package/src/__tests__/deterministic-id.test.ts +39 -0
- package/src/__tests__/doctor.test.ts +303 -0
- package/src/__tests__/drill-down.test.ts +108 -0
- package/src/__tests__/embedder.test.ts +122 -0
- package/src/__tests__/entity-resolver.test.ts +58 -0
- package/src/__tests__/event-segmenter.test.ts +153 -0
- package/src/__tests__/hotness-blend.test.ts +76 -0
- package/src/__tests__/ingest.test.ts +360 -0
- package/src/__tests__/kg-extractor.test.ts +184 -0
- package/src/__tests__/kg-store.test.ts +198 -0
- package/src/__tests__/llm-consolidation.test.ts +226 -0
- package/src/__tests__/memory-boundaries.test.ts +237 -0
- package/src/__tests__/memory-health-rebalance.test.ts +144 -0
- package/src/__tests__/memory-output.test.ts +215 -0
- package/src/__tests__/multi-vector.test.ts +242 -0
- package/src/__tests__/ppr-traversal.test.ts +256 -0
- package/src/__tests__/preference-guard-smoke.test.ts +211 -0
- package/src/__tests__/preference-matcher.test.ts +230 -0
- package/src/__tests__/preference-slots.test.ts +106 -0
- package/src/__tests__/prospective-memory.test.ts +283 -0
- package/src/__tests__/query-entity-detector.test.ts +171 -0
- package/src/__tests__/query-expander.test.ts +34 -0
- package/src/__tests__/recall-mode.test.ts +64 -0
- package/src/__tests__/result-synthesizer.test.ts +198 -0
- package/src/__tests__/resume-context.test.ts +4743 -0
- package/src/__tests__/retrieval-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
- package/src/__tests__/retroactive-boost.test.ts +223 -0
- package/src/__tests__/scope-inventory.test.ts +200 -0
- package/src/__tests__/scope-policy.test.ts +69 -0
- package/src/__tests__/session-checkpoint.test.ts +259 -0
- package/src/__tests__/session-gc.test.ts +215 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/temporal-parser.test.ts +241 -0
- package/src/__tests__/term-registry.test.ts +121 -0
- package/src/__tests__/tool-output-compressor.test.ts +133 -0
- package/src/__tests__/version-manager.test.ts +213 -0
- package/src/__tests__/workflow-observation.test.ts +400 -0
- package/src/access-tracker.ts +316 -0
- package/src/adaptive-retrieval.ts +64 -0
- package/src/api-server.ts +667 -0
- package/src/asset-sync.ts +79 -0
- package/src/auto-consolidation.ts +133 -0
- package/src/auto-gc.ts +124 -0
- package/src/auto-recall.ts +95 -0
- package/src/capture-engine.ts +931 -0
- package/src/capture-heuristic.ts +177 -0
- package/src/cascade-forget.ts +135 -0
- package/src/chunker.ts +253 -0
- package/src/cli.ts +1917 -0
- package/src/confidence-tracker.ts +166 -0
- package/src/conflict-advisor.ts +433 -0
- package/src/conflict-engine.ts +181 -0
- package/src/conflict-escalation.ts +138 -0
- package/src/conflict-lifecycle.ts +67 -0
- package/src/conflict-output.ts +218 -0
- package/src/conflict-schema.ts +130 -0
- package/src/conflict-store.ts +127 -0
- package/src/consolidation-engine.ts +256 -0
- package/src/context-composer-pins.ts +183 -0
- package/src/context-composer-queries.ts +121 -0
- package/src/context-composer-scope.ts +56 -0
- package/src/context-composer-stable-selection.ts +200 -0
- package/src/context-composer-stable.ts +217 -0
- package/src/context-composer-task-fallbacks.ts +36 -0
- package/src/context-composer-task-ranking.ts +197 -0
- package/src/context-composer-task-results.ts +137 -0
- package/src/context-composer-task-selection.ts +342 -0
- package/src/context-composer-text.ts +57 -0
- package/src/context-composer.ts +300 -0
- package/src/context-renderer.ts +179 -0
- package/src/decay-engine.ts +189 -0
- package/src/doctor.ts +543 -0
- package/src/embedder.ts +557 -0
- package/src/entity-resolver.ts +131 -0
- package/src/eval.ts +632 -0
- package/src/event-segmenter.ts +216 -0
- package/src/ingest.ts +1822 -0
- package/src/kg-extractor.ts +217 -0
- package/src/kg-store.ts +353 -0
- package/src/legacy-scope-review.ts +53 -0
- package/src/llm-client.ts +629 -0
- package/src/llm-consolidation.ts +168 -0
- package/src/mcp-server.ts +1337 -0
- package/src/memory-assets.ts +475 -0
- package/src/memory-boundaries.ts +354 -0
- package/src/memory-health-rebalance.ts +220 -0
- package/src/memory-output.ts +474 -0
- package/src/memory-schema.ts +184 -0
- package/src/multi-vector.ts +234 -0
- package/src/noise-filter.ts +155 -0
- package/src/ppr-traversal.ts +279 -0
- package/src/preference-matcher.ts +174 -0
- package/src/preference-slots.ts +259 -0
- package/src/prospective-memory.ts +198 -0
- package/src/query-entity-detector.ts +207 -0
- package/src/query-expander.ts +98 -0
- package/src/result-synthesizer.ts +114 -0
- package/src/retrieval-profiles.ts +101 -0
- package/src/retrieval-trace.ts +94 -0
- package/src/retriever.ts +1338 -0
- package/src/retroactive-boost.ts +154 -0
- package/src/rif.ts +73 -0
- package/src/runtime-config.ts +172 -0
- package/src/schema-utils.ts +56 -0
- package/src/scope-inventory.ts +310 -0
- package/src/scope-policy.ts +142 -0
- package/src/session-engine.ts +168 -0
- package/src/session-output.ts +96 -0
- package/src/session-schema.ts +60 -0
- package/src/session-store.ts +293 -0
- package/src/stderr-log.ts +16 -0
- package/src/store.ts +903 -0
- package/src/temporal-parser.ts +318 -0
- package/src/term-registry.ts +791 -0
- package/src/tool-output-compressor.ts +115 -0
- package/src/tracker.ts +57 -0
- package/src/ui-server.ts +430 -0
- package/src/version-manager.ts +172 -0
- package/src/workflow-observation-engine.ts +316 -0
- package/src/workflow-observation-managed.ts +83 -0
- package/src/workflow-observation-output.ts +86 -0
- package/src/workflow-observation-schema.ts +87 -0
- package/src/workflow-observation-scope-review.ts +409 -0
- package/src/workflow-observation-store.ts +69 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Graph Triple Extractor
|
|
3
|
+
* Uses LLM few-shot OpenIE to extract (subject, predicate, object) triples from memory text.
|
|
4
|
+
* Gated by RECALLNEST_KG_MODE env var.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { LLMClient } from "./llm-client.js";
|
|
8
|
+
import type { KGStore } from "./kg-store.js";
|
|
9
|
+
import { logInfo, logWarn } from "./stderr-log.js";
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Types
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
export interface RawTriple {
|
|
16
|
+
subject: string;
|
|
17
|
+
predicate: string;
|
|
18
|
+
object: string;
|
|
19
|
+
confidence: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface KGExtractorConfig {
|
|
23
|
+
/** Minimum confidence to keep a triple (0-1). Default: 0.6 */
|
|
24
|
+
minConfidence?: number;
|
|
25
|
+
/** LLM client for extraction */
|
|
26
|
+
llmClient: LLMClient;
|
|
27
|
+
/** KG store for persisting triples */
|
|
28
|
+
kgStore: KGStore;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface LlmExtractionResponse {
|
|
32
|
+
triples: RawTriple[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Feature Gate
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
/** Check if KG mode is enabled (env var) */
|
|
40
|
+
export function isKGModeEnabled(): boolean {
|
|
41
|
+
return process.env.RECALLNEST_KG_MODE === "true";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// Entity Normalization
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Normalize entity names for consistent matching:
|
|
50
|
+
* - Trim whitespace, collapse internal whitespace
|
|
51
|
+
* - Title case for Latin text, CJK unchanged
|
|
52
|
+
* - Strip surrounding quotes/brackets
|
|
53
|
+
*/
|
|
54
|
+
export function normalizeEntity(raw: string): string {
|
|
55
|
+
let entity = raw.trim();
|
|
56
|
+
|
|
57
|
+
// Strip surrounding quotes and brackets
|
|
58
|
+
entity = entity.replace(/^[\s"'""''「」『』【】\[\]()()]+/, "");
|
|
59
|
+
entity = entity.replace(/[\s"'""''「」『』【】\[\]()()]+$/, "");
|
|
60
|
+
|
|
61
|
+
// Collapse whitespace
|
|
62
|
+
entity = entity.replace(/\s+/g, " ").trim();
|
|
63
|
+
|
|
64
|
+
if (!entity) return "";
|
|
65
|
+
|
|
66
|
+
// For CJK text, return as-is
|
|
67
|
+
if (/[\u4e00-\u9fff\u3400-\u4dbf]/.test(entity)) {
|
|
68
|
+
return entity;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Title Case for English entities
|
|
72
|
+
return entity
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.replace(/\b\w/g, (ch) => ch.toUpperCase());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Normalize predicate to snake_case for consistency.
|
|
79
|
+
*/
|
|
80
|
+
export function normalizePredicate(raw: string): string {
|
|
81
|
+
return raw
|
|
82
|
+
.trim()
|
|
83
|
+
.toLowerCase()
|
|
84
|
+
.replace(/[\s-]+/g, "_")
|
|
85
|
+
.replace(/[^a-z0-9_\u4e00-\u9fff]/g, "");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// Extraction Prompt
|
|
90
|
+
// ============================================================================
|
|
91
|
+
|
|
92
|
+
const KG_EXTRACT_SYSTEM = `You are a knowledge graph extraction assistant. Extract (subject, predicate, object) triples from text. Respond with valid JSON only.`;
|
|
93
|
+
|
|
94
|
+
function buildKGExtractionPrompt(text: string): string {
|
|
95
|
+
return `Extract knowledge graph triples from the text below.
|
|
96
|
+
|
|
97
|
+
Rules:
|
|
98
|
+
- Extract relationships between named entities, concepts, tools, people, projects
|
|
99
|
+
- Use simple predicates: uses, created_by, works_with, friend_of, depends_on, located_in, member_of, prefers, knows, discussed, mentioned_in, related_to, is_a, has, manages, built, wrote, lives_in
|
|
100
|
+
- Assign confidence (0-1) based on how explicit the relationship is
|
|
101
|
+
- Skip vague or speculative relationships (confidence < 0.5)
|
|
102
|
+
- 3-8 triples per text typically
|
|
103
|
+
|
|
104
|
+
Examples:
|
|
105
|
+
|
|
106
|
+
Text: "Alice uses Python for her data science projects at Google"
|
|
107
|
+
{"triples":[{"subject":"Alice","predicate":"uses","object":"Python","confidence":0.95},{"subject":"Alice","predicate":"works_at","object":"Google","confidence":0.90}]}
|
|
108
|
+
|
|
109
|
+
Text: "RecallNest depends on LanceDB for vector storage and uses OpenAI for embeddings"
|
|
110
|
+
{"triples":[{"subject":"RecallNest","predicate":"depends_on","object":"LanceDB","confidence":0.95},{"subject":"RecallNest","predicate":"uses","object":"OpenAI","confidence":0.90}]}
|
|
111
|
+
|
|
112
|
+
Now extract triples from:
|
|
113
|
+
|
|
114
|
+
${text}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// Extractor
|
|
119
|
+
// ============================================================================
|
|
120
|
+
|
|
121
|
+
export class KGExtractor {
|
|
122
|
+
private readonly minConfidence: number;
|
|
123
|
+
private readonly llm: LLMClient;
|
|
124
|
+
private readonly kgStore: KGStore;
|
|
125
|
+
|
|
126
|
+
constructor(config: KGExtractorConfig) {
|
|
127
|
+
this.minConfidence = config.minConfidence ?? 0.6;
|
|
128
|
+
this.llm = config.llmClient;
|
|
129
|
+
this.kgStore = config.kgStore;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Extract triples from text and persist to KG store.
|
|
134
|
+
* Returns the number of triples stored.
|
|
135
|
+
*/
|
|
136
|
+
async extractAndStore(
|
|
137
|
+
text: string,
|
|
138
|
+
sourceMemoryId: string,
|
|
139
|
+
scope: string,
|
|
140
|
+
): Promise<number> {
|
|
141
|
+
const rawTriples = await this.extract(text);
|
|
142
|
+
if (rawTriples.length === 0) return 0;
|
|
143
|
+
|
|
144
|
+
const triples = await this.kgStore.createTriples(
|
|
145
|
+
rawTriples.map((t) => ({
|
|
146
|
+
scope,
|
|
147
|
+
subject: t.subject,
|
|
148
|
+
predicate: t.predicate,
|
|
149
|
+
object: t.object,
|
|
150
|
+
confidence: t.confidence,
|
|
151
|
+
source_memory_id: sourceMemoryId,
|
|
152
|
+
source_text: text.slice(0, 500), // Cap source text
|
|
153
|
+
})),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
logInfo(`[KG] stored ${triples.length} triples from memory ${sourceMemoryId}`);
|
|
157
|
+
return triples.length;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Extract raw triples from text via LLM. Does not persist.
|
|
162
|
+
*/
|
|
163
|
+
async extract(text: string): Promise<RawTriple[]> {
|
|
164
|
+
if (!text || text.length < 10) return [];
|
|
165
|
+
|
|
166
|
+
const prompt = buildKGExtractionPrompt(text);
|
|
167
|
+
|
|
168
|
+
let response: LlmExtractionResponse | null = null;
|
|
169
|
+
try {
|
|
170
|
+
response = await this.llm.chatJson<LlmExtractionResponse>(
|
|
171
|
+
KG_EXTRACT_SYSTEM,
|
|
172
|
+
prompt,
|
|
173
|
+
);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
logWarn(`[KG] LLM extraction failed: ${String(err)}`);
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!response?.triples || !Array.isArray(response.triples)) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Validate, normalize, filter
|
|
184
|
+
const valid: RawTriple[] = [];
|
|
185
|
+
for (const t of response.triples) {
|
|
186
|
+
if (!t.subject || !t.predicate || !t.object) continue;
|
|
187
|
+
|
|
188
|
+
const subject = normalizeEntity(t.subject);
|
|
189
|
+
const predicate = normalizePredicate(t.predicate);
|
|
190
|
+
const object = normalizeEntity(t.object);
|
|
191
|
+
const confidence = Number(t.confidence) || 0;
|
|
192
|
+
|
|
193
|
+
if (!subject || !predicate || !object) continue;
|
|
194
|
+
if (confidence < this.minConfidence) continue;
|
|
195
|
+
if (subject === object) continue; // Skip self-referencing
|
|
196
|
+
|
|
197
|
+
valid.push({ subject, predicate, object, confidence });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Deduplicate within batch
|
|
201
|
+
const seen = new Set<string>();
|
|
202
|
+
return valid.filter((t) => {
|
|
203
|
+
const key = `${t.subject}\x00${t.predicate}\x00${t.object}`;
|
|
204
|
+
if (seen.has(key)) return false;
|
|
205
|
+
seen.add(key);
|
|
206
|
+
return true;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ============================================================================
|
|
212
|
+
// Factory
|
|
213
|
+
// ============================================================================
|
|
214
|
+
|
|
215
|
+
export function createKGExtractor(config: KGExtractorConfig): KGExtractor {
|
|
216
|
+
return new KGExtractor(config);
|
|
217
|
+
}
|
package/src/kg-store.ts
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Graph Triple Store
|
|
3
|
+
* Stores (subject, predicate, object) triples in a dedicated LanceDB table.
|
|
4
|
+
* Pure relational — no vector columns.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type * as LanceDB from "@lancedb/lancedb";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { loadLanceDB } from "./store.js";
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Types
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
export interface KGTriple {
|
|
16
|
+
/** Deterministic ID: sha256(scope + subject + predicate + object) */
|
|
17
|
+
id: string;
|
|
18
|
+
scope: string;
|
|
19
|
+
subject: string;
|
|
20
|
+
predicate: string;
|
|
21
|
+
object: string;
|
|
22
|
+
/** Extraction confidence 0-1 */
|
|
23
|
+
confidence: number;
|
|
24
|
+
/** ID of the source memory entry */
|
|
25
|
+
source_memory_id: string;
|
|
26
|
+
/** Original text snippet used for extraction */
|
|
27
|
+
source_text: string;
|
|
28
|
+
timestamp: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface KGStoreConfig {
|
|
32
|
+
/** Reuse the same LanceDB connection path as MemoryStore */
|
|
33
|
+
dbPath: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface NeighborhoodResult {
|
|
37
|
+
entity: string;
|
|
38
|
+
triples: KGTriple[];
|
|
39
|
+
/** How many hops from the seed entity */
|
|
40
|
+
hops: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Helpers
|
|
45
|
+
// ============================================================================
|
|
46
|
+
|
|
47
|
+
const KG_TABLE_NAME = "kg_triples";
|
|
48
|
+
|
|
49
|
+
/** Deterministic triple ID for dedup */
|
|
50
|
+
export function tripleId(scope: string, subject: string, predicate: string, object: string): string {
|
|
51
|
+
const raw = `${scope}\x00${subject}\x00${predicate}\x00${object}`;
|
|
52
|
+
return createHash("sha256").update(raw).digest("hex").slice(0, 32);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function escapeSql(value: string): string {
|
|
56
|
+
return value.replace(/'/g, "''");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// KG Store
|
|
61
|
+
// ============================================================================
|
|
62
|
+
|
|
63
|
+
export class KGStore {
|
|
64
|
+
private db: LanceDB.Connection | null = null;
|
|
65
|
+
private table: LanceDB.Table | null = null;
|
|
66
|
+
private initPromise: Promise<void> | null = null;
|
|
67
|
+
|
|
68
|
+
constructor(private readonly config: KGStoreConfig) {}
|
|
69
|
+
|
|
70
|
+
// --------------------------------------------------------------------------
|
|
71
|
+
// Initialization
|
|
72
|
+
// --------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
private async ensureInitialized(): Promise<void> {
|
|
75
|
+
if (this.table) return;
|
|
76
|
+
if (this.initPromise) return this.initPromise;
|
|
77
|
+
|
|
78
|
+
this.initPromise = this.doInitialize().catch((err) => {
|
|
79
|
+
this.initPromise = null;
|
|
80
|
+
throw err;
|
|
81
|
+
});
|
|
82
|
+
return this.initPromise;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async doInitialize(): Promise<void> {
|
|
86
|
+
const lancedb = await loadLanceDB();
|
|
87
|
+
const db = await lancedb.connect(this.config.dbPath);
|
|
88
|
+
|
|
89
|
+
let table: LanceDB.Table;
|
|
90
|
+
try {
|
|
91
|
+
table = await db.openTable(KG_TABLE_NAME);
|
|
92
|
+
} catch {
|
|
93
|
+
// Table doesn't exist — create with schema entry then remove it
|
|
94
|
+
const schemaEntry: KGTriple = {
|
|
95
|
+
id: "__schema__",
|
|
96
|
+
scope: "global",
|
|
97
|
+
subject: "",
|
|
98
|
+
predicate: "",
|
|
99
|
+
object: "",
|
|
100
|
+
confidence: 0,
|
|
101
|
+
source_memory_id: "",
|
|
102
|
+
source_text: "",
|
|
103
|
+
timestamp: 0,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
table = await db.createTable(KG_TABLE_NAME, [schemaEntry]);
|
|
108
|
+
await table.delete('id = "__schema__"');
|
|
109
|
+
} catch (createErr) {
|
|
110
|
+
if (String(createErr).includes("already exists")) {
|
|
111
|
+
table = await db.openTable(KG_TABLE_NAME);
|
|
112
|
+
} else {
|
|
113
|
+
throw createErr;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Create FTS index on subject for entity lookup (optional)
|
|
119
|
+
try {
|
|
120
|
+
const indices = await table.listIndices();
|
|
121
|
+
const hasSubjectFts = indices?.some(
|
|
122
|
+
(idx: any) => idx.indexType === "FTS" && idx.columns?.includes("subject"),
|
|
123
|
+
);
|
|
124
|
+
if (!hasSubjectFts) {
|
|
125
|
+
const lance = await loadLanceDB();
|
|
126
|
+
await table.createIndex("subject", { config: (lance as any).Index.fts() });
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
// FTS on subject is optional — BFS still works without it
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.db = db;
|
|
133
|
+
this.table = table;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --------------------------------------------------------------------------
|
|
137
|
+
// Write
|
|
138
|
+
// --------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
async createTriple(triple: Omit<KGTriple, "id" | "timestamp">): Promise<KGTriple> {
|
|
141
|
+
await this.ensureInitialized();
|
|
142
|
+
|
|
143
|
+
const full: KGTriple = {
|
|
144
|
+
...triple,
|
|
145
|
+
id: tripleId(triple.scope, triple.subject, triple.predicate, triple.object),
|
|
146
|
+
timestamp: Date.now(),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Upsert: delete old then add new (LanceDB has no native upsert)
|
|
150
|
+
try {
|
|
151
|
+
await this.table!.delete(`id = '${escapeSql(full.id)}'`);
|
|
152
|
+
} catch {
|
|
153
|
+
// OK if not found
|
|
154
|
+
}
|
|
155
|
+
await this.table!.add([full]);
|
|
156
|
+
return full;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async createTriples(triples: Array<Omit<KGTriple, "id" | "timestamp">>): Promise<KGTriple[]> {
|
|
160
|
+
if (triples.length === 0) return [];
|
|
161
|
+
await this.ensureInitialized();
|
|
162
|
+
|
|
163
|
+
const now = Date.now();
|
|
164
|
+
const fullTriples: KGTriple[] = triples.map((t) => ({
|
|
165
|
+
...t,
|
|
166
|
+
id: tripleId(t.scope, t.subject, t.predicate, t.object),
|
|
167
|
+
timestamp: now,
|
|
168
|
+
}));
|
|
169
|
+
|
|
170
|
+
// Deduplicate by id (same triple in batch)
|
|
171
|
+
const seen = new Set<string>();
|
|
172
|
+
const deduped = fullTriples.filter((t) => {
|
|
173
|
+
if (seen.has(t.id)) return false;
|
|
174
|
+
seen.add(t.id);
|
|
175
|
+
return true;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Batch delete existing then add
|
|
179
|
+
const ids = deduped.map((t) => `'${escapeSql(t.id)}'`).join(", ");
|
|
180
|
+
try {
|
|
181
|
+
await this.table!.delete(`id IN (${ids})`);
|
|
182
|
+
} catch {
|
|
183
|
+
// OK if none found
|
|
184
|
+
}
|
|
185
|
+
await this.table!.add(deduped);
|
|
186
|
+
return deduped;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// --------------------------------------------------------------------------
|
|
190
|
+
// Read — Edge queries
|
|
191
|
+
// --------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
/** Get all triples where entity is the subject */
|
|
194
|
+
async getOutgoingEdges(entity: string, scope?: string): Promise<KGTriple[]> {
|
|
195
|
+
await this.ensureInitialized();
|
|
196
|
+
|
|
197
|
+
let filter = `subject = '${escapeSql(entity)}'`;
|
|
198
|
+
if (scope) filter += ` AND scope = '${escapeSql(scope)}'`;
|
|
199
|
+
|
|
200
|
+
const rows = await this.table!.query().where(filter).limit(500).toArray();
|
|
201
|
+
return rows.map(rowToTriple);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Get all triples where entity is the object */
|
|
205
|
+
async getIncomingEdges(entity: string, scope?: string): Promise<KGTriple[]> {
|
|
206
|
+
await this.ensureInitialized();
|
|
207
|
+
|
|
208
|
+
let filter = `object = '${escapeSql(entity)}'`;
|
|
209
|
+
if (scope) filter += ` AND scope = '${escapeSql(scope)}'`;
|
|
210
|
+
|
|
211
|
+
const rows = await this.table!.query().where(filter).limit(500).toArray();
|
|
212
|
+
return rows.map(rowToTriple);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* BFS neighborhood traversal up to `maxHops` hops.
|
|
217
|
+
* Returns all triples reachable from the seed entity.
|
|
218
|
+
*/
|
|
219
|
+
async getNeighborhood(
|
|
220
|
+
seedEntities: string[],
|
|
221
|
+
maxHops = 2,
|
|
222
|
+
scope?: string,
|
|
223
|
+
): Promise<NeighborhoodResult[]> {
|
|
224
|
+
await this.ensureInitialized();
|
|
225
|
+
|
|
226
|
+
const visited = new Map<string, number>(); // entity -> min hops
|
|
227
|
+
const allTriples: KGTriple[] = [];
|
|
228
|
+
let frontier = [...seedEntities];
|
|
229
|
+
|
|
230
|
+
for (const seed of seedEntities) {
|
|
231
|
+
visited.set(seed, 0);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
for (let hop = 1; hop <= maxHops; hop++) {
|
|
235
|
+
if (frontier.length === 0) break;
|
|
236
|
+
|
|
237
|
+
const nextFrontier: string[] = [];
|
|
238
|
+
|
|
239
|
+
for (const entity of frontier) {
|
|
240
|
+
const [outgoing, incoming] = await Promise.all([
|
|
241
|
+
this.getOutgoingEdges(entity, scope),
|
|
242
|
+
this.getIncomingEdges(entity, scope),
|
|
243
|
+
]);
|
|
244
|
+
|
|
245
|
+
for (const t of [...outgoing, ...incoming]) {
|
|
246
|
+
allTriples.push(t);
|
|
247
|
+
|
|
248
|
+
for (const neighbor of [t.subject, t.object]) {
|
|
249
|
+
if (!visited.has(neighbor)) {
|
|
250
|
+
visited.set(neighbor, hop);
|
|
251
|
+
nextFrontier.push(neighbor);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
frontier = nextFrontier;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Group by entity
|
|
261
|
+
const results: NeighborhoodResult[] = [];
|
|
262
|
+
for (const [entity, hops] of visited) {
|
|
263
|
+
const related = allTriples.filter(
|
|
264
|
+
(t) => t.subject === entity || t.object === entity,
|
|
265
|
+
);
|
|
266
|
+
if (related.length > 0 || seedEntities.includes(entity)) {
|
|
267
|
+
results.push({ entity, triples: related, hops });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return results;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// --------------------------------------------------------------------------
|
|
275
|
+
// Read — Entity listing
|
|
276
|
+
// --------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
/** Get all unique entities in scope */
|
|
279
|
+
async getAllEntities(scope?: string): Promise<string[]> {
|
|
280
|
+
await this.ensureInitialized();
|
|
281
|
+
|
|
282
|
+
const filter = scope ? `scope = '${escapeSql(scope)}'` : "1=1";
|
|
283
|
+
const rows = await this.table!.query()
|
|
284
|
+
.select(["subject", "object"])
|
|
285
|
+
.where(filter)
|
|
286
|
+
.limit(10000)
|
|
287
|
+
.toArray();
|
|
288
|
+
|
|
289
|
+
const entities = new Set<string>();
|
|
290
|
+
for (const row of rows) {
|
|
291
|
+
if (row.subject) entities.add(row.subject as string);
|
|
292
|
+
if (row.object) entities.add(row.object as string);
|
|
293
|
+
}
|
|
294
|
+
return [...entities];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Check if an entity exists in the KG */
|
|
298
|
+
async hasEntity(entity: string, scope?: string): Promise<boolean> {
|
|
299
|
+
await this.ensureInitialized();
|
|
300
|
+
|
|
301
|
+
const safeEntity = escapeSql(entity);
|
|
302
|
+
let filter = `subject = '${safeEntity}' OR object = '${safeEntity}'`;
|
|
303
|
+
if (scope) filter = `(${filter}) AND scope = '${escapeSql(scope)}'`;
|
|
304
|
+
|
|
305
|
+
const rows = await this.table!.query().where(filter).limit(1).toArray();
|
|
306
|
+
return rows.length > 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// --------------------------------------------------------------------------
|
|
310
|
+
// Delete
|
|
311
|
+
// --------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
/** Delete all triples extracted from a given source memory */
|
|
314
|
+
async deleteBySource(sourceMemoryId: string): Promise<void> {
|
|
315
|
+
await this.ensureInitialized();
|
|
316
|
+
await this.table!.delete(`source_memory_id = '${escapeSql(sourceMemoryId)}'`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Delete all triples in scope */
|
|
320
|
+
async deleteByScope(scope: string): Promise<void> {
|
|
321
|
+
await this.ensureInitialized();
|
|
322
|
+
await this.table!.delete(`scope = '${escapeSql(scope)}'`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// --------------------------------------------------------------------------
|
|
326
|
+
// Stats
|
|
327
|
+
// --------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
async countTriples(scope?: string): Promise<number> {
|
|
330
|
+
await this.ensureInitialized();
|
|
331
|
+
const filter = scope ? `scope = '${escapeSql(scope)}'` : "1=1";
|
|
332
|
+
const rows = await this.table!.query().where(filter).select(["id"]).limit(100000).toArray();
|
|
333
|
+
return rows.length;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ============================================================================
|
|
338
|
+
// Row mapping
|
|
339
|
+
// ============================================================================
|
|
340
|
+
|
|
341
|
+
function rowToTriple(row: Record<string, unknown>): KGTriple {
|
|
342
|
+
return {
|
|
343
|
+
id: row.id as string,
|
|
344
|
+
scope: (row.scope as string) ?? "global",
|
|
345
|
+
subject: row.subject as string,
|
|
346
|
+
predicate: row.predicate as string,
|
|
347
|
+
object: row.object as string,
|
|
348
|
+
confidence: Number(row.confidence),
|
|
349
|
+
source_memory_id: row.source_memory_id as string,
|
|
350
|
+
source_text: row.source_text as string,
|
|
351
|
+
timestamp: Number(row.timestamp),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { LegacyScopeIssueKind } from "./store.js";
|
|
2
|
+
|
|
3
|
+
export interface LegacyScopeReviewRecord {
|
|
4
|
+
decision: "keep";
|
|
5
|
+
kind: LegacyScopeIssueKind;
|
|
6
|
+
reason: string;
|
|
7
|
+
reviewedAt: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
11
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
12
|
+
return value as Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function parseLegacyScopeReview(raw: unknown): LegacyScopeReviewRecord | undefined {
|
|
16
|
+
const record = asObject(raw);
|
|
17
|
+
if (!record) return undefined;
|
|
18
|
+
if (record.decision !== "keep") return undefined;
|
|
19
|
+
if (record.kind !== "missing" && record.kind !== "empty" && record.kind !== "global") return undefined;
|
|
20
|
+
if (typeof record.reason !== "string" || !record.reason.trim()) return undefined;
|
|
21
|
+
if (typeof record.reviewedAt !== "string" || !Number.isFinite(Date.parse(record.reviewedAt))) return undefined;
|
|
22
|
+
return {
|
|
23
|
+
decision: "keep",
|
|
24
|
+
kind: record.kind,
|
|
25
|
+
reason: record.reason.trim(),
|
|
26
|
+
reviewedAt: record.reviewedAt,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildLegacyScopeKeepReview(
|
|
31
|
+
kind: LegacyScopeIssueKind,
|
|
32
|
+
reason: string,
|
|
33
|
+
reviewedAt = new Date().toISOString(),
|
|
34
|
+
): LegacyScopeReviewRecord {
|
|
35
|
+
const normalizedReason = reason.trim();
|
|
36
|
+
if (!normalizedReason) {
|
|
37
|
+
throw new Error("legacy scope keep review reason must not be empty.");
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
decision: "keep",
|
|
41
|
+
kind,
|
|
42
|
+
reason: normalizedReason,
|
|
43
|
+
reviewedAt,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function suppressesLegacyScopeIssue(
|
|
48
|
+
raw: unknown,
|
|
49
|
+
kind: LegacyScopeIssueKind,
|
|
50
|
+
): boolean {
|
|
51
|
+
const review = parseLegacyScopeReview(raw);
|
|
52
|
+
return review?.decision === "keep" && review.kind === kind;
|
|
53
|
+
}
|