pi-mega-compact 0.4.21 → 0.4.24
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/dist/extensions/dashboard-server.js +60 -9
- package/dist/extensions/dashboard-server.test.js +77 -0
- package/dist/extensions/mega-compact-driver.js +79 -0
- package/dist/extensions/mega-compact.test.js +54 -18
- package/dist/extensions/mega-config.js +10 -0
- package/dist/extensions/mega-dashboard-cmds.js +32 -2
- package/dist/extensions/mega-events.js +45 -23
- package/dist/extensions/mega-pipeline.js +77 -3
- package/dist/src/config/dedup.js +4 -1
- package/dist/src/config.js +21 -0
- package/dist/src/dedup/raptor/index.js +28 -6
- package/dist/src/dedup/raptor/promote.test.js +69 -0
- package/dist/src/engine.js +1 -0
- package/dist/src/recall.js +30 -4
- package/dist/src/recall.test.js +28 -0
- package/dist/src/store/backfill.js +5 -6
- package/dist/src/store/compression.js +47 -7
- package/dist/src/store/compression.test.js +48 -0
- package/dist/src/store/sqlite.js +64 -41
- package/dist/src/store.test.js +19 -0
- package/dist/src/vectorStore.js +56 -1
- package/extensions/DASHBOARD.md +3 -3
- package/extensions/dashboard-server.test.ts +77 -0
- package/extensions/dashboard-server.ts +57 -11
- package/extensions/mega-compact-driver.ts +105 -0
- package/extensions/mega-compact.test.ts +65 -18
- package/extensions/mega-config.ts +25 -0
- package/extensions/mega-dashboard-cmds.ts +23 -2
- package/extensions/mega-events.ts +43 -24
- package/extensions/mega-pipeline.ts +83 -4
- package/package.json +6 -7
- package/src/config/dedup.ts +4 -1
- package/src/config.ts +26 -0
- package/src/dedup/raptor/index.ts +42 -7
- package/src/dedup/raptor/promote.test.ts +82 -0
- package/src/engine.ts +5 -0
- package/src/recall.test.ts +44 -0
- package/src/recall.ts +43 -4
- package/src/store/backfill.ts +10 -11
- package/src/store/compression.test.ts +58 -0
- package/src/store/compression.ts +48 -7
- package/src/store/sqlite.ts +72 -49
- package/src/store.test.ts +22 -0
- package/src/vectorStore.ts +63 -1
- package/dist/extensions/openclaw-mega-compact.js +0 -291
- package/dist/src/minilm.js +0 -92
- package/dist/src/wordpiece.js +0 -129
package/dist/src/minilm.js
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* minilm.ts — local MiniLM (all-MiniLM-L6-v2) sentence embedder (Sprint 12).
|
|
3
|
-
*
|
|
4
|
-
* Implements the `Embedder` interface so it drops into the existing VectorStore
|
|
5
|
-
* dedup cascade and search with no call-site changes. Inference is 100% local:
|
|
6
|
-
* the ONNX model + WordPiece vocab are on-disk artifacts fetched once by
|
|
7
|
-
* scripts/setup-minilm.mjs. There is NO network call at runtime (PREVENT-PI-004).
|
|
8
|
-
*
|
|
9
|
-
* Inputs (dynamic): input_ids, attention_mask, token_type_ids (int64).
|
|
10
|
-
* Output: last_hidden_state (batch, seq, 384). We mean-pool over non-padded
|
|
11
|
-
* tokens (attention_mask == 1) and L2-normalize → 384-dim unit vector.
|
|
12
|
-
*
|
|
13
|
-
* The ONNX session + tokenizer are loaded LAZILY on first embed() so the default
|
|
14
|
-
* TrigramEmbedder path (and its zero native-init cost) is untouched unless
|
|
15
|
-
* MEGACOMPACT_EMBEDDER=minilm is selected.
|
|
16
|
-
*/
|
|
17
|
-
import { join } from "node:path";
|
|
18
|
-
import { homedir } from "node:os";
|
|
19
|
-
import { existsSync } from "node:fs";
|
|
20
|
-
import { l2Normalize, awaitSync } from "./embedder.js";
|
|
21
|
-
import { WordPieceTokenizer } from "./wordpiece.js";
|
|
22
|
-
export const MINILM_DIM = 384;
|
|
23
|
-
export const MINILM_MAX_LEN = 256;
|
|
24
|
-
/** Resolve the model directory: MEGACOMPACT_MINILM_DIR > ./models/minilm > ~/.pi … */
|
|
25
|
-
function resolveModelDir() {
|
|
26
|
-
if (process.env.MEGACOMPACT_MINILM_DIR)
|
|
27
|
-
return process.env.MEGACOMPACT_MINILM_DIR;
|
|
28
|
-
// Repo-local vendored path (gitignored).
|
|
29
|
-
const local = join(process.cwd(), "models", "minilm");
|
|
30
|
-
if (existsSync(local))
|
|
31
|
-
return local;
|
|
32
|
-
return join(homedir(), ".pi", "agent", "extensions", "mega-compact", "models", "minilm");
|
|
33
|
-
}
|
|
34
|
-
export class MiniLMEmbedder {
|
|
35
|
-
dim = MINILM_DIM;
|
|
36
|
-
session = null;
|
|
37
|
-
tokenizer = null;
|
|
38
|
-
modelDir;
|
|
39
|
-
loadPromise = null;
|
|
40
|
-
constructor(modelDir = resolveModelDir()) {
|
|
41
|
-
this.modelDir = modelDir;
|
|
42
|
-
}
|
|
43
|
-
async ensureLoaded() {
|
|
44
|
-
if (this.session && this.tokenizer)
|
|
45
|
-
return;
|
|
46
|
-
if (this.loadPromise)
|
|
47
|
-
return this.loadPromise;
|
|
48
|
-
this.loadPromise = (async () => {
|
|
49
|
-
const ort = await import("onnxruntime-node");
|
|
50
|
-
const modelPath = join(this.modelDir, "model_quantized.onnx");
|
|
51
|
-
const vocabPath = join(this.modelDir, "vocab.txt");
|
|
52
|
-
if (!existsSync(modelPath) || !existsSync(vocabPath)) {
|
|
53
|
-
throw new Error(`MiniLM artifacts missing in ${this.modelDir}. Run: node scripts/setup-minilm.mjs`);
|
|
54
|
-
}
|
|
55
|
-
// 1 thread is plenty for a single short-region embed and bounds CPU.
|
|
56
|
-
this.session = await ort.InferenceSession.create(modelPath, {
|
|
57
|
-
executionProviders: ["cpu"],
|
|
58
|
-
graphOptimizationLevel: "all",
|
|
59
|
-
});
|
|
60
|
-
this.tokenizer = WordPieceTokenizer.fromVocabFile(vocabPath);
|
|
61
|
-
})();
|
|
62
|
-
return this.loadPromise;
|
|
63
|
-
}
|
|
64
|
-
embed(text) {
|
|
65
|
-
awaitSync(this.ensureLoaded());
|
|
66
|
-
const enc = this.tokenizer.encode(text, MINILM_MAX_LEN);
|
|
67
|
-
const n = enc.inputIds.length;
|
|
68
|
-
const BigInt64 = (arr) => arr.map((x) => BigInt(x));
|
|
69
|
-
const ort = awaitSync(import("onnxruntime-node"));
|
|
70
|
-
const tensors = {
|
|
71
|
-
input_ids: new ort.Tensor("int64", BigInt64(enc.inputIds), [1, n]),
|
|
72
|
-
attention_mask: new ort.Tensor("int64", BigInt64(enc.attentionMask), [1, n]),
|
|
73
|
-
token_type_ids: new ort.Tensor("int64", BigInt64(enc.tokenTypeIds), [1, n]),
|
|
74
|
-
};
|
|
75
|
-
const out = awaitSync(this.session.run(tensors));
|
|
76
|
-
const hidden = out.last_hidden_state.data;
|
|
77
|
-
// hidden shape: [1, n, 384]. Mean-pool over non-padded positions.
|
|
78
|
-
const pooled = new Array(MINILM_DIM).fill(0);
|
|
79
|
-
let count = 0;
|
|
80
|
-
for (let i = 0; i < n; i++) {
|
|
81
|
-
if (enc.attentionMask[i] === 0)
|
|
82
|
-
continue;
|
|
83
|
-
const base = i * MINILM_DIM;
|
|
84
|
-
for (let d = 0; d < MINILM_DIM; d++)
|
|
85
|
-
pooled[d] += hidden[base + d];
|
|
86
|
-
count++;
|
|
87
|
-
}
|
|
88
|
-
if (count === 0)
|
|
89
|
-
return l2Normalize(new Array(MINILM_DIM).fill(0));
|
|
90
|
-
return l2Normalize(pooled.map((x) => x / count));
|
|
91
|
-
}
|
|
92
|
-
}
|
package/dist/src/wordpiece.js
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* wordpiece.ts — a self-contained WordPiece tokenizer for BERT/MiniLM.
|
|
3
|
-
*
|
|
4
|
-
* Loads the canonical `vocab.txt` (bert-base-uncased, ~30K tokens) from disk and
|
|
5
|
-
* implements the standard uncased BERT preprocessing + greedy longest-match
|
|
6
|
-
* WordPiece segmentation. No native dependency, no network — the vocab file is a
|
|
7
|
-
* local artifact fetched once by scripts/setup-minilm.mjs (PREVENT-PI-004).
|
|
8
|
-
*
|
|
9
|
-
* This mirrors HuggingFace `BertTokenizer` closely enough for sentence-embedding
|
|
10
|
-
* use: lowercase, strip accents, split on whitespace + punctuation, then
|
|
11
|
-
* WordPiece each token with the `##` continuation convention. Special tokens
|
|
12
|
-
* [CLS]/[SEP] are added by the caller's encode().
|
|
13
|
-
*/
|
|
14
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
15
|
-
const UNK = "[UNK]";
|
|
16
|
-
const CLS = "[CLS]";
|
|
17
|
-
const SEP = "[SEP]";
|
|
18
|
-
const PAD = "[PAD]";
|
|
19
|
-
const MAX_INPUT_CHARS_PER_WORD = 200;
|
|
20
|
-
export class WordPieceTokenizer {
|
|
21
|
-
vocab;
|
|
22
|
-
clsId;
|
|
23
|
-
sepId;
|
|
24
|
-
padId;
|
|
25
|
-
unkId;
|
|
26
|
-
constructor(vocab) {
|
|
27
|
-
this.vocab = vocab;
|
|
28
|
-
this.clsId = vocab.get(CLS) ?? 101;
|
|
29
|
-
this.sepId = vocab.get(SEP) ?? 102;
|
|
30
|
-
this.padId = vocab.get(PAD) ?? 0;
|
|
31
|
-
this.unkId = vocab.get(UNK) ?? 100;
|
|
32
|
-
}
|
|
33
|
-
/** Build a tokenizer from a vocab.txt file (one token per line, index = line). */
|
|
34
|
-
static fromVocabFile(path) {
|
|
35
|
-
if (!existsSync(path)) {
|
|
36
|
-
throw new Error(`WordPiece vocab not found at ${path}. Run: node scripts/setup-minilm.mjs`);
|
|
37
|
-
}
|
|
38
|
-
const lines = readFileSync(path, "utf-8").split("\n");
|
|
39
|
-
const vocab = new Map();
|
|
40
|
-
for (let i = 0; i < lines.length; i++) {
|
|
41
|
-
const tok = lines[i].replace(/\r$/, "");
|
|
42
|
-
if (tok.length > 0 || i < lines.length - 1)
|
|
43
|
-
vocab.set(tok, i);
|
|
44
|
-
}
|
|
45
|
-
return new WordPieceTokenizer(vocab);
|
|
46
|
-
}
|
|
47
|
-
/** Uncased BERT basic tokenization: lowercase, strip accents, split on ws+punct. */
|
|
48
|
-
basicTokenize(text) {
|
|
49
|
-
// NFD + strip combining marks (accent removal), then lowercase.
|
|
50
|
-
const cleaned = text
|
|
51
|
-
.normalize("NFD")
|
|
52
|
-
.replace(/[̀-ͯ]/g, "")
|
|
53
|
-
.toLowerCase();
|
|
54
|
-
const tokens = [];
|
|
55
|
-
let buf = "";
|
|
56
|
-
const flush = () => {
|
|
57
|
-
if (buf.length > 0) {
|
|
58
|
-
tokens.push(buf);
|
|
59
|
-
buf = "";
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
for (const ch of cleaned) {
|
|
63
|
-
if (/\s/.test(ch)) {
|
|
64
|
-
flush();
|
|
65
|
-
}
|
|
66
|
-
else if (/[!-/:-@[-`{-~¡-¿]/.test(ch)) {
|
|
67
|
-
// Punctuation becomes its own token.
|
|
68
|
-
flush();
|
|
69
|
-
tokens.push(ch);
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
buf += ch;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
flush();
|
|
76
|
-
return tokens;
|
|
77
|
-
}
|
|
78
|
-
/** Greedy longest-match WordPiece for a single word. */
|
|
79
|
-
wordpiece(word) {
|
|
80
|
-
if (word.length > MAX_INPUT_CHARS_PER_WORD)
|
|
81
|
-
return [UNK];
|
|
82
|
-
const pieces = [];
|
|
83
|
-
let start = 0;
|
|
84
|
-
while (start < word.length) {
|
|
85
|
-
let end = word.length;
|
|
86
|
-
let cur = null;
|
|
87
|
-
while (start < end) {
|
|
88
|
-
let sub = word.slice(start, end);
|
|
89
|
-
if (start > 0)
|
|
90
|
-
sub = "##" + sub;
|
|
91
|
-
if (this.vocab.has(sub)) {
|
|
92
|
-
cur = sub;
|
|
93
|
-
break;
|
|
94
|
-
}
|
|
95
|
-
end--;
|
|
96
|
-
}
|
|
97
|
-
if (cur === null)
|
|
98
|
-
return [UNK]; // any unmatchable piece → whole word is UNK
|
|
99
|
-
pieces.push(cur);
|
|
100
|
-
start = end;
|
|
101
|
-
}
|
|
102
|
-
return pieces;
|
|
103
|
-
}
|
|
104
|
-
/** Tokenize text into WordPiece token strings (no special tokens). */
|
|
105
|
-
tokenize(text) {
|
|
106
|
-
const out = [];
|
|
107
|
-
for (const word of this.basicTokenize(text)) {
|
|
108
|
-
for (const piece of this.wordpiece(word))
|
|
109
|
-
out.push(piece);
|
|
110
|
-
}
|
|
111
|
-
return out;
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Encode text into model inputs with [CLS]…[SEP], truncated to `maxLen`.
|
|
115
|
-
* attention_mask is all 1s (no padding for single-sequence inference).
|
|
116
|
-
*/
|
|
117
|
-
encode(text, maxLen = 256) {
|
|
118
|
-
const pieces = this.tokenize(text).slice(0, Math.max(0, maxLen - 2));
|
|
119
|
-
const inputIds = [this.clsId];
|
|
120
|
-
for (const p of pieces)
|
|
121
|
-
inputIds.push(this.vocab.get(p) ?? this.unkId);
|
|
122
|
-
inputIds.push(this.sepId);
|
|
123
|
-
return {
|
|
124
|
-
inputIds,
|
|
125
|
-
attentionMask: inputIds.map(() => 1),
|
|
126
|
-
tokenTypeIds: inputIds.map(() => 0),
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
}
|