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,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Vector L0/L1/L2 Embedding Support
|
|
3
|
+
*
|
|
4
|
+
* Borrowed from UltraMemory's multi-vector approach:
|
|
5
|
+
* - L0 (abstract): 1-sentence concept-level summary → good for broad topic match
|
|
6
|
+
* - L1 (overview): bullet-point structure → good for structural match
|
|
7
|
+
* - L2 (detail): full content → default vector (already exists)
|
|
8
|
+
*
|
|
9
|
+
* RecallNest already stores L0/L1/L2 text in metadata. This module:
|
|
10
|
+
* 1. Extracts L0/L1 text from metadata
|
|
11
|
+
* 2. Generates additional vector embeddings for them
|
|
12
|
+
* 3. Provides a blending function for retrieval scoring
|
|
13
|
+
*
|
|
14
|
+
* Opt-in via RECALLNEST_MULTI_VECTOR=true environment variable.
|
|
15
|
+
* When disabled, all functions are no-ops that return empty results.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Embedder } from "./embedder.js";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Config
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export function isMultiVectorEnabled(): boolean {
|
|
25
|
+
return process.env.RECALLNEST_MULTI_VECTOR === "true";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Types
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
export interface MultiVectorEmbeddings {
|
|
33
|
+
/** L0 abstract vector (concept-level). Null if no L0 text available. */
|
|
34
|
+
vector_l0: number[] | null;
|
|
35
|
+
/** L1 overview vector (structure-level). Null if no L1 text available. */
|
|
36
|
+
vector_l1: number[] | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface MultiVectorBlendConfig {
|
|
40
|
+
/** Weight for main vector channel (default: 0.65) */
|
|
41
|
+
vectorWeight: number;
|
|
42
|
+
/** Weight for L0 abstract channel (default: 0.20) */
|
|
43
|
+
l0Weight: number;
|
|
44
|
+
/** Weight for L1 overview channel (default: 0.15) */
|
|
45
|
+
l1Weight: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const DEFAULT_BLEND_CONFIG: MultiVectorBlendConfig = {
|
|
49
|
+
vectorWeight: 0.65,
|
|
50
|
+
l0Weight: 0.20,
|
|
51
|
+
l1Weight: 0.15,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Extraction
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/** Extract L0 and L1 text from metadata JSON string. */
|
|
59
|
+
export function extractMultiVectorText(metadata?: string): { l0?: string; l1?: string } {
|
|
60
|
+
if (!metadata) return {};
|
|
61
|
+
try {
|
|
62
|
+
const meta = JSON.parse(metadata);
|
|
63
|
+
return {
|
|
64
|
+
l0: typeof meta.l0_abstract === "string" && meta.l0_abstract.length > 5 ? meta.l0_abstract : undefined,
|
|
65
|
+
l1: typeof meta.l1_overview === "string" && meta.l1_overview.length > 5 ? meta.l1_overview : undefined,
|
|
66
|
+
};
|
|
67
|
+
} catch {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Embedding
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Generate L0 and L1 vector embeddings from metadata text.
|
|
78
|
+
* Returns null vectors when text is not available or multi-vector is disabled.
|
|
79
|
+
*/
|
|
80
|
+
export async function embedMultiVector(
|
|
81
|
+
embedder: Pick<Embedder, "embedPassage">,
|
|
82
|
+
metadata?: string,
|
|
83
|
+
): Promise<MultiVectorEmbeddings> {
|
|
84
|
+
if (!isMultiVectorEnabled()) {
|
|
85
|
+
return { vector_l0: null, vector_l1: null };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const { l0, l1 } = extractMultiVectorText(metadata);
|
|
89
|
+
|
|
90
|
+
const [vector_l0, vector_l1] = await Promise.all([
|
|
91
|
+
l0 ? embedder.embedPassage(l0) : Promise.resolve(null),
|
|
92
|
+
l1 ? embedder.embedPassage(l1) : Promise.resolve(null),
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
return { vector_l0, vector_l1 };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Score Blending
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Blend multi-vector scores into a single relevance score.
|
|
104
|
+
*
|
|
105
|
+
* When L0/L1 scores are available, the main vector score is weighted down
|
|
106
|
+
* and supplemented by abstract/overview channel scores.
|
|
107
|
+
* When L0/L1 are unavailable, falls back to main vector score only.
|
|
108
|
+
*/
|
|
109
|
+
export function blendMultiVectorScores(
|
|
110
|
+
mainScore: number,
|
|
111
|
+
l0Score: number | null,
|
|
112
|
+
l1Score: number | null,
|
|
113
|
+
config: MultiVectorBlendConfig = DEFAULT_BLEND_CONFIG,
|
|
114
|
+
): number {
|
|
115
|
+
if (l0Score === null && l1Score === null) {
|
|
116
|
+
return mainScore; // No multi-vector data, use main score as-is
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Redistribute weights among available channels
|
|
120
|
+
let totalWeight = config.vectorWeight;
|
|
121
|
+
let weightedSum = mainScore * config.vectorWeight;
|
|
122
|
+
|
|
123
|
+
if (l0Score !== null) {
|
|
124
|
+
totalWeight += config.l0Weight;
|
|
125
|
+
weightedSum += l0Score * config.l0Weight;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (l1Score !== null) {
|
|
129
|
+
totalWeight += config.l1Weight;
|
|
130
|
+
weightedSum += l1Score * config.l1Weight;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return totalWeight > 0 ? weightedSum / totalWeight : mainScore;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Cosine Similarity (for in-memory multi-vector scoring)
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
/** Compute cosine similarity between two vectors. Returns 0 for mismatched/empty vectors. */
|
|
141
|
+
export function cosineSimilarity(a: number[], b: number[]): number {
|
|
142
|
+
if (a.length !== b.length || a.length === 0) return 0;
|
|
143
|
+
let dot = 0, normA = 0, normB = 0;
|
|
144
|
+
for (let i = 0; i < a.length; i++) {
|
|
145
|
+
dot += a[i] * b[i];
|
|
146
|
+
normA += a[i] * a[i];
|
|
147
|
+
normB += b[i] * b[i];
|
|
148
|
+
}
|
|
149
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
150
|
+
return denom === 0 ? 0 : dot / denom;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Text-Overlap Scoring (lightweight proxy for L0/L1 vector similarity)
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
const CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf]/;
|
|
158
|
+
const STOP_WORDS_EN = new Set(["a","an","the","is","are","was","were","be","been","being","have","has","had","do","does","did","will","would","shall","should","may","might","can","could","of","in","to","for","with","on","at","by","from","and","or","but","not","no","so","if","as","it","i","my","me","we","our","us","you","your","he","she","they","them","this","that","what","which","who","how","when","where","why"]);
|
|
159
|
+
const STOP_WORDS_ZH = new Set(["的","了","在","是","我","有","和","就","不","人","都","一","一个","上","也","很","到","说","要","去","你","会","着","没有","看","好","自己","这"]);
|
|
160
|
+
|
|
161
|
+
/** Tokenize text into words, CJK-aware. Returns lowercased tokens with stop words removed. */
|
|
162
|
+
export function tokenize(text: string): string[] {
|
|
163
|
+
const tokens: string[] = [];
|
|
164
|
+
const lower = text.toLowerCase();
|
|
165
|
+
|
|
166
|
+
// Walk character-by-character: CJK chars → individual tokens, Latin runs → whole-word tokens
|
|
167
|
+
let currentWord = "";
|
|
168
|
+
for (const ch of lower) {
|
|
169
|
+
if (CJK_RANGE.test(ch)) {
|
|
170
|
+
// Flush any accumulated Latin word
|
|
171
|
+
if (currentWord.length > 1 && !STOP_WORDS_EN.has(currentWord)) tokens.push(currentWord);
|
|
172
|
+
currentWord = "";
|
|
173
|
+
// CJK char as individual token
|
|
174
|
+
if (!STOP_WORDS_ZH.has(ch)) tokens.push(ch);
|
|
175
|
+
} else if (/\w/.test(ch)) {
|
|
176
|
+
currentWord += ch;
|
|
177
|
+
} else {
|
|
178
|
+
// Punctuation/whitespace: flush Latin word
|
|
179
|
+
if (currentWord.length > 1 && !STOP_WORDS_EN.has(currentWord)) tokens.push(currentWord);
|
|
180
|
+
currentWord = "";
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Flush trailing Latin word
|
|
184
|
+
if (currentWord.length > 1 && !STOP_WORDS_EN.has(currentWord)) tokens.push(currentWord);
|
|
185
|
+
|
|
186
|
+
return tokens;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Compute term-overlap score between query and candidate text.
|
|
191
|
+
* Returns [0, 1] blending coverage (query terms found) and density
|
|
192
|
+
* (query terms / candidate length). Density rewards concise abstracts
|
|
193
|
+
* over verbose full-text, which is exactly the L0>L1>L2 gradient we want.
|
|
194
|
+
*/
|
|
195
|
+
export function textOverlapScore(query: string, candidateText: string): number {
|
|
196
|
+
const qTokens = tokenize(query);
|
|
197
|
+
if (qTokens.length === 0) return 0;
|
|
198
|
+
|
|
199
|
+
const cTokens = tokenize(candidateText);
|
|
200
|
+
if (cTokens.length === 0) return 0;
|
|
201
|
+
|
|
202
|
+
const cSet = new Set(cTokens);
|
|
203
|
+
let hits = 0;
|
|
204
|
+
for (const t of qTokens) {
|
|
205
|
+
if (cSet.has(t)) hits++;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Coverage: what fraction of query terms appear in candidate [0, 1]
|
|
209
|
+
const coverage = hits / qTokens.length;
|
|
210
|
+
|
|
211
|
+
// Density: query term hits / candidate token count — rewards concise text [0, 1]
|
|
212
|
+
const density = hits / cTokens.length;
|
|
213
|
+
|
|
214
|
+
// Blend: coverage dominant (70%), density as tiebreaker (30%)
|
|
215
|
+
return coverage * 0.7 + density * 0.3;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Adaptive blend weights based on query length.
|
|
220
|
+
* Short/conceptual queries → weight L0/L1 more.
|
|
221
|
+
* Long/detailed queries → weight L2 (main vector) more.
|
|
222
|
+
*/
|
|
223
|
+
export function adaptiveBlendConfig(queryTokenCount: number): MultiVectorBlendConfig {
|
|
224
|
+
if (queryTokenCount <= 3) {
|
|
225
|
+
// Topic-level: boost L0/L1
|
|
226
|
+
return { vectorWeight: 0.50, l0Weight: 0.25, l1Weight: 0.25 };
|
|
227
|
+
}
|
|
228
|
+
if (queryTokenCount <= 10) {
|
|
229
|
+
// Default balanced
|
|
230
|
+
return DEFAULT_BLEND_CONFIG;
|
|
231
|
+
}
|
|
232
|
+
// Detailed query: rely on L2 main vector
|
|
233
|
+
return { vectorWeight: 0.80, l0Weight: 0.10, l1Weight: 0.10 };
|
|
234
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Noise Filter
|
|
3
|
+
* Filters out low-quality memories (meta-questions, agent denials, session boilerplate)
|
|
4
|
+
* Inspired by openclaw-plugin-continuity's noise filtering approach.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { logInfo } from "./stderr-log.js";
|
|
8
|
+
|
|
9
|
+
// Agent-side denial patterns
|
|
10
|
+
const DENIAL_PATTERNS = [
|
|
11
|
+
/i don'?t have (any )?(information|data|memory|record)/i,
|
|
12
|
+
/i'?m not sure about/i,
|
|
13
|
+
/i don'?t recall/i,
|
|
14
|
+
/i don'?t remember/i,
|
|
15
|
+
/it looks like i don'?t/i,
|
|
16
|
+
/i wasn'?t able to find/i,
|
|
17
|
+
/no (relevant )?memories found/i,
|
|
18
|
+
/i don'?t have access to/i,
|
|
19
|
+
// Chinese denial patterns (synced from upstream)
|
|
20
|
+
/我没有(任何)?(相关)?(信息|数据|记忆|记录)/,
|
|
21
|
+
/我不(太)?确定/,
|
|
22
|
+
/我不记得/,
|
|
23
|
+
/我想不起来/,
|
|
24
|
+
/我没(有)?找到/,
|
|
25
|
+
/找不到(相关)?记忆/,
|
|
26
|
+
/没有(相关)?记忆/,
|
|
27
|
+
/我无法(访问|获取)/,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// User-side meta-question patterns (about memory itself, not content)
|
|
31
|
+
const META_QUESTION_PATTERNS = [
|
|
32
|
+
/\bdo you (remember|recall|know about)\b/i,
|
|
33
|
+
/\bcan you (remember|recall)\b/i,
|
|
34
|
+
/\bdid i (tell|mention|say|share)\b/i,
|
|
35
|
+
/\bhave i (told|mentioned|said)\b/i,
|
|
36
|
+
/\bwhat did i (tell|say|mention)\b/i,
|
|
37
|
+
// Chinese meta-question patterns (synced from upstream)
|
|
38
|
+
/你(还)?记得吗/,
|
|
39
|
+
/你(还)?记不记得/,
|
|
40
|
+
/你知道我(说过|提过|告诉|提到).*吗/,
|
|
41
|
+
/我(有没有|是不是)(说过|提过|告诉|提到)/,
|
|
42
|
+
/我之前(说过|提过|提到|告诉)/,
|
|
43
|
+
/我(跟你)?说过.*吗/,
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// Session boilerplate (anchored to prevent false positives on real content)
|
|
47
|
+
const BOILERPLATE_PATTERNS = [
|
|
48
|
+
/^(hi|hello|hey|good morning|good evening|greetings)/i,
|
|
49
|
+
/^fresh session/i,
|
|
50
|
+
/^new session/i,
|
|
51
|
+
/^HEARTBEAT/i,
|
|
52
|
+
/^你好[!!\s,.,。]?$/,
|
|
53
|
+
/^(早上好|早安|午安|晚上好|晚安)[!!\s,.,。]?$/,
|
|
54
|
+
/^(嗨|哈[喽啰]|哈[喽啰]呀)[!!\s,.,。]?$/,
|
|
55
|
+
/^(嗯|哦|噢|呃)[!!\s,.,。]?$/,
|
|
56
|
+
/^新(会话|对话|聊天)/,
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// Short boilerplate: only noise when total text is very short
|
|
60
|
+
// "好的" alone = noise, "好的方案是使用Redis" = real content
|
|
61
|
+
const SHORT_BOILERPLATE_PATTERNS = [
|
|
62
|
+
/^(好的|好吧|行|可以|没问题|OK|ok|收到|明白|了解|知道了)/i,
|
|
63
|
+
/^(谢谢|感谢|多谢|谢啦|3Q|thx)/i,
|
|
64
|
+
];
|
|
65
|
+
const BOILERPLATE_MAX_LENGTH = 10;
|
|
66
|
+
|
|
67
|
+
// OpenClaw v3.2+ injected metadata headers (backport from v1.0.29)
|
|
68
|
+
const METADATA_HEADER_PATTERNS = [
|
|
69
|
+
/^Conversation info \(untrusted metadata\)/i,
|
|
70
|
+
/^---\s*\n\s*\{[\s\S]*?\}\s*\n\s*---/m, // YAML-like JSON blocks
|
|
71
|
+
/^\[?\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^\]]*\]?\s*$/m, // bare timestamps
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
// Diagnostic artifact patterns (synced from upstream v1.1.0-beta.3)
|
|
75
|
+
// Extractor validation outputs that leak into memory store
|
|
76
|
+
const DIAGNOSTIC_ARTIFACT_PATTERNS = [
|
|
77
|
+
/^query\s*->\s*none$/i,
|
|
78
|
+
/^no explicit solution$/i,
|
|
79
|
+
/^如果你知道.+只回复/i,
|
|
80
|
+
/^如果不知道.+只回复\s*none/i,
|
|
81
|
+
/^只回复精确代号/i,
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
export interface NoiseFilterOptions {
|
|
85
|
+
/** Filter agent denial responses (default: true) */
|
|
86
|
+
filterDenials?: boolean;
|
|
87
|
+
/** Filter meta-questions about memory (default: true) */
|
|
88
|
+
filterMetaQuestions?: boolean;
|
|
89
|
+
/** Filter session boilerplate (default: true) */
|
|
90
|
+
filterBoilerplate?: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const DEFAULT_OPTIONS: Required<NoiseFilterOptions> = {
|
|
94
|
+
filterDenials: true,
|
|
95
|
+
filterMetaQuestions: true,
|
|
96
|
+
filterBoilerplate: true,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if a memory text is noise that should be filtered out.
|
|
101
|
+
* Returns true if the text is noise.
|
|
102
|
+
*/
|
|
103
|
+
export function isNoise(text: string, options: NoiseFilterOptions = {}): boolean {
|
|
104
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
105
|
+
const trimmed = text.trim();
|
|
106
|
+
|
|
107
|
+
if (trimmed.length < 5) {
|
|
108
|
+
logInfo(`[INFO] noise-filter: skipped short text (${trimmed.length} chars)`);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (opts.filterDenials && DENIAL_PATTERNS.some(p => p.test(trimmed))) {
|
|
113
|
+
logInfo(`[INFO] noise-filter: denial pattern matched: "${trimmed.slice(0, 60)}..."`);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
if (opts.filterMetaQuestions && META_QUESTION_PATTERNS.some(p => p.test(trimmed))) {
|
|
117
|
+
logInfo(`[INFO] noise-filter: meta-question filtered: "${trimmed.slice(0, 60)}..."`);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
if (opts.filterBoilerplate) {
|
|
121
|
+
if (BOILERPLATE_PATTERNS.some(p => p.test(trimmed))) {
|
|
122
|
+
logInfo(`[INFO] noise-filter: boilerplate filtered: "${trimmed.slice(0, 60)}..."`);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
if (trimmed.length <= BOILERPLATE_MAX_LENGTH &&
|
|
126
|
+
SHORT_BOILERPLATE_PATTERNS.some(p => p.test(trimmed))) {
|
|
127
|
+
logInfo(`[INFO] noise-filter: short boilerplate filtered: "${trimmed}"`);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// OpenClaw v3.2+ metadata noise (backport from v1.0.29)
|
|
132
|
+
if (METADATA_HEADER_PATTERNS.some(p => p.test(trimmed))) {
|
|
133
|
+
logInfo(`[INFO] noise-filter: metadata header filtered`);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
// Diagnostic artifact noise (synced from upstream v1.1.0-beta.3)
|
|
137
|
+
if (DIAGNOSTIC_ARTIFACT_PATTERNS.some(p => p.test(trimmed))) {
|
|
138
|
+
logInfo(`[INFO] noise-filter: diagnostic artifact filtered: "${trimmed.slice(0, 60)}..."`);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Filter an array of items, removing noise entries.
|
|
147
|
+
*/
|
|
148
|
+
export function filterNoise<T>(
|
|
149
|
+
items: T[],
|
|
150
|
+
getText: (item: T) => string,
|
|
151
|
+
options?: NoiseFilterOptions
|
|
152
|
+
): T[] {
|
|
153
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
154
|
+
return items.filter(item => !isNoise(getText(item), opts));
|
|
155
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Personalized PageRank (PPR) Graph Traversal
|
|
3
|
+
* Walks the KG from seed entities and scores reachable nodes by relevance.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { KGTriple, NeighborhoodResult } from "./kg-store.js";
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Types
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
export interface PPRConfig {
|
|
13
|
+
/** Teleport probability back to seed nodes (default: 0.85) */
|
|
14
|
+
dampingFactor: number;
|
|
15
|
+
/** Max power-iteration rounds (default: 10) */
|
|
16
|
+
maxIterations: number;
|
|
17
|
+
/** Early stop when max score delta < threshold (default: 0.001) */
|
|
18
|
+
convergenceThreshold: number;
|
|
19
|
+
/** Return top-K scored entities (default: 20) */
|
|
20
|
+
topK: number;
|
|
21
|
+
/** Ignore paths longer than this many hops (default: 2) */
|
|
22
|
+
hopLimit: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PPRResult {
|
|
26
|
+
entity: string;
|
|
27
|
+
score: number;
|
|
28
|
+
/** Shortest hop distance from any seed entity */
|
|
29
|
+
hops: number;
|
|
30
|
+
/** One shortest path from seed to this entity */
|
|
31
|
+
path: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const DEFAULT_PPR_CONFIG: PPRConfig = {
|
|
35
|
+
dampingFactor: 0.85,
|
|
36
|
+
maxIterations: 10,
|
|
37
|
+
convergenceThreshold: 0.001,
|
|
38
|
+
topK: 20,
|
|
39
|
+
hopLimit: 2,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// Edge weights by predicate
|
|
44
|
+
// ============================================================================
|
|
45
|
+
|
|
46
|
+
const PREDICATE_WEIGHTS: Record<string, number> = {
|
|
47
|
+
created_by: 0.95,
|
|
48
|
+
depends_on: 0.90,
|
|
49
|
+
uses: 0.90,
|
|
50
|
+
works_with: 0.80,
|
|
51
|
+
works_at: 0.80,
|
|
52
|
+
friend_of: 0.75,
|
|
53
|
+
member_of: 0.75,
|
|
54
|
+
knows: 0.75,
|
|
55
|
+
manages: 0.75,
|
|
56
|
+
built: 0.85,
|
|
57
|
+
wrote: 0.85,
|
|
58
|
+
prefers: 0.70,
|
|
59
|
+
discussed: 0.70,
|
|
60
|
+
lives_in: 0.70,
|
|
61
|
+
located_in: 0.70,
|
|
62
|
+
mentioned_in: 0.60,
|
|
63
|
+
related_to: 0.50,
|
|
64
|
+
is_a: 0.80,
|
|
65
|
+
has: 0.75,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const DEFAULT_EDGE_WEIGHT = 0.60;
|
|
69
|
+
|
|
70
|
+
export function edgeWeight(predicate: string): number {
|
|
71
|
+
return PREDICATE_WEIGHTS[predicate] ?? DEFAULT_EDGE_WEIGHT;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ============================================================================
|
|
75
|
+
// Graph builder from NeighborhoodResult
|
|
76
|
+
// ============================================================================
|
|
77
|
+
|
|
78
|
+
interface AdjEntry {
|
|
79
|
+
neighbor: string;
|
|
80
|
+
weight: number;
|
|
81
|
+
predicate: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface Graph {
|
|
85
|
+
/** entity -> outgoing adjacency list */
|
|
86
|
+
adj: Map<string, AdjEntry[]>;
|
|
87
|
+
/** All nodes in the graph */
|
|
88
|
+
nodes: Set<string>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build an undirected weighted graph from BFS neighborhood results.
|
|
93
|
+
* Each triple creates edges in both directions.
|
|
94
|
+
*/
|
|
95
|
+
export function buildGraph(neighborhood: NeighborhoodResult[]): Graph {
|
|
96
|
+
const adj = new Map<string, AdjEntry[]>();
|
|
97
|
+
const nodes = new Set<string>();
|
|
98
|
+
const seen = new Set<string>(); // dedup triples by id
|
|
99
|
+
|
|
100
|
+
for (const nr of neighborhood) {
|
|
101
|
+
nodes.add(nr.entity);
|
|
102
|
+
for (const t of nr.triples) {
|
|
103
|
+
if (seen.has(t.id)) continue;
|
|
104
|
+
seen.add(t.id);
|
|
105
|
+
|
|
106
|
+
nodes.add(t.subject);
|
|
107
|
+
nodes.add(t.object);
|
|
108
|
+
|
|
109
|
+
const w = edgeWeight(t.predicate) * t.confidence;
|
|
110
|
+
|
|
111
|
+
// subject -> object
|
|
112
|
+
if (!adj.has(t.subject)) adj.set(t.subject, []);
|
|
113
|
+
adj.get(t.subject)!.push({ neighbor: t.object, weight: w, predicate: t.predicate });
|
|
114
|
+
|
|
115
|
+
// object -> subject (undirected)
|
|
116
|
+
if (!adj.has(t.object)) adj.set(t.object, []);
|
|
117
|
+
adj.get(t.object)!.push({ neighbor: t.subject, weight: w, predicate: t.predicate });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { adj, nodes };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ============================================================================
|
|
125
|
+
// PPR core
|
|
126
|
+
// ============================================================================
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Run Personalized PageRank on a pre-built graph.
|
|
130
|
+
*
|
|
131
|
+
* Algorithm:
|
|
132
|
+
* score(v) = (1-d) * seed(v) + d * Σ_u [ score(u) * w(u→v) / outWeightSum(u) ]
|
|
133
|
+
*
|
|
134
|
+
* where d = dampingFactor, seed(v) = 1/|seeds| if v is a seed entity, else 0.
|
|
135
|
+
*/
|
|
136
|
+
export function pprTraverse(
|
|
137
|
+
graph: Graph,
|
|
138
|
+
startEntities: string[],
|
|
139
|
+
config: Partial<PPRConfig> = {},
|
|
140
|
+
): PPRResult[] {
|
|
141
|
+
const cfg = { ...DEFAULT_PPR_CONFIG, ...config };
|
|
142
|
+
const { dampingFactor: d, maxIterations, convergenceThreshold, topK } = cfg;
|
|
143
|
+
|
|
144
|
+
if (graph.nodes.size === 0 || startEntities.length === 0) {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Filter seeds to only those present in graph
|
|
149
|
+
const seeds = startEntities.filter(e => graph.nodes.has(e));
|
|
150
|
+
if (seeds.length === 0) return [];
|
|
151
|
+
|
|
152
|
+
const seedSet = new Set(seeds);
|
|
153
|
+
const seedScore = 1.0 / seeds.length;
|
|
154
|
+
|
|
155
|
+
// Initialize scores: uniform over seeds
|
|
156
|
+
const scores = new Map<string, number>();
|
|
157
|
+
for (const node of graph.nodes) {
|
|
158
|
+
scores.set(node, seedSet.has(node) ? seedScore : 0);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Pre-compute out-weight sums
|
|
162
|
+
const outWeightSum = new Map<string, number>();
|
|
163
|
+
for (const [node, edges] of graph.adj) {
|
|
164
|
+
let sum = 0;
|
|
165
|
+
for (const e of edges) sum += e.weight;
|
|
166
|
+
outWeightSum.set(node, sum);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Power iteration
|
|
170
|
+
for (let iter = 0; iter < maxIterations; iter++) {
|
|
171
|
+
const newScores = new Map<string, number>();
|
|
172
|
+
|
|
173
|
+
// Teleport component
|
|
174
|
+
for (const node of graph.nodes) {
|
|
175
|
+
newScores.set(node, seedSet.has(node) ? (1 - d) * seedScore : 0);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Diffusion component
|
|
179
|
+
for (const [node, edges] of graph.adj) {
|
|
180
|
+
const nodeScore = scores.get(node) ?? 0;
|
|
181
|
+
if (nodeScore === 0) continue;
|
|
182
|
+
const totalOut = outWeightSum.get(node) ?? 1;
|
|
183
|
+
|
|
184
|
+
for (const edge of edges) {
|
|
185
|
+
const contribution = d * nodeScore * (edge.weight / totalOut);
|
|
186
|
+
newScores.set(edge.neighbor, (newScores.get(edge.neighbor) ?? 0) + contribution);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check convergence
|
|
191
|
+
let maxDelta = 0;
|
|
192
|
+
for (const node of graph.nodes) {
|
|
193
|
+
const delta = Math.abs((newScores.get(node) ?? 0) - (scores.get(node) ?? 0));
|
|
194
|
+
if (delta > maxDelta) maxDelta = delta;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Update scores
|
|
198
|
+
for (const [node, score] of newScores) {
|
|
199
|
+
scores.set(node, score);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (maxDelta < convergenceThreshold) break;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Compute shortest paths from seeds via BFS
|
|
206
|
+
const hopsMap = new Map<string, number>();
|
|
207
|
+
const pathMap = new Map<string, string[]>();
|
|
208
|
+
|
|
209
|
+
for (const seed of seeds) {
|
|
210
|
+
hopsMap.set(seed, 0);
|
|
211
|
+
pathMap.set(seed, [seed]);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const bfsQueue = [...seeds];
|
|
215
|
+
let qi = 0;
|
|
216
|
+
while (qi < bfsQueue.length) {
|
|
217
|
+
const current = bfsQueue[qi++];
|
|
218
|
+
const currentHops = hopsMap.get(current)!;
|
|
219
|
+
if (currentHops >= cfg.hopLimit) continue;
|
|
220
|
+
|
|
221
|
+
const edges = graph.adj.get(current) ?? [];
|
|
222
|
+
for (const edge of edges) {
|
|
223
|
+
if (!hopsMap.has(edge.neighbor)) {
|
|
224
|
+
hopsMap.set(edge.neighbor, currentHops + 1);
|
|
225
|
+
pathMap.set(edge.neighbor, [...(pathMap.get(current) ?? []), edge.neighbor]);
|
|
226
|
+
bfsQueue.push(edge.neighbor);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Build results, exclude seed entities themselves, sort by score
|
|
232
|
+
const results: PPRResult[] = [];
|
|
233
|
+
for (const [entity, score] of scores) {
|
|
234
|
+
if (score <= 0) continue;
|
|
235
|
+
const hops = hopsMap.get(entity) ?? Infinity;
|
|
236
|
+
if (hops > cfg.hopLimit) continue;
|
|
237
|
+
|
|
238
|
+
results.push({
|
|
239
|
+
entity,
|
|
240
|
+
score,
|
|
241
|
+
hops,
|
|
242
|
+
path: pathMap.get(entity) ?? [entity],
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Sort by score descending, take top-K
|
|
247
|
+
results.sort((a, b) => b.score - a.score);
|
|
248
|
+
return results.slice(0, topK);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ============================================================================
|
|
252
|
+
// Convenience: from triples directly
|
|
253
|
+
// ============================================================================
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Build graph from raw triples and run PPR.
|
|
257
|
+
* Useful when you already have triples without going through KGStore.getNeighborhood.
|
|
258
|
+
*/
|
|
259
|
+
export function pprFromTriples(
|
|
260
|
+
triples: KGTriple[],
|
|
261
|
+
startEntities: string[],
|
|
262
|
+
config?: Partial<PPRConfig>,
|
|
263
|
+
): PPRResult[] {
|
|
264
|
+
// Convert triples to a NeighborhoodResult-like structure
|
|
265
|
+
const entities = new Set<string>();
|
|
266
|
+
for (const t of triples) {
|
|
267
|
+
entities.add(t.subject);
|
|
268
|
+
entities.add(t.object);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const neighborhood: NeighborhoodResult[] = [...entities].map(e => ({
|
|
272
|
+
entity: e,
|
|
273
|
+
triples: triples.filter(t => t.subject === e || t.object === e),
|
|
274
|
+
hops: 0,
|
|
275
|
+
}));
|
|
276
|
+
|
|
277
|
+
const graph = buildGraph(neighborhood);
|
|
278
|
+
return pprTraverse(graph, startEntities, config);
|
|
279
|
+
}
|