kibi-plugin-search-hints 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/LICENSE.md +661 -0
- package/README.md +23 -2
- package/SBOM.spdx.json +64 -0
- package/THIRD_PARTY_NOTICES.md +7 -0
- package/assets/potion-base-8M.int8.bin +0 -0
- package/assets/vocab.txt +29528 -0
- package/dist/cache.d.ts +25 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +112 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +71 -0
- package/dist/model.d.ts +18 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +56 -0
- package/dist/tokenizer.d.ts +17 -0
- package/dist/tokenizer.d.ts.map +1 -0
- package/dist/tokenizer.js +135 -0
- package/integrity.json +128 -0
- package/licenses/bge-base-en-v1.5-vocabulary-MIT.txt +21 -0
- package/licenses/potion-base-8M-MIT.txt +21 -0
- package/package.json +54 -4
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SearchHintsEntity } from "kibi-plugin-sdk";
|
|
2
|
+
/**
|
|
3
|
+
* Entity vectors keyed by type::id and the host content hash. A changed
|
|
4
|
+
* entity, a different model or a damaged file means that vector (or the
|
|
5
|
+
* whole file) is rebuilt; nothing stale is ever returned.
|
|
6
|
+
*/
|
|
7
|
+
export declare class VectorCache {
|
|
8
|
+
private readonly file;
|
|
9
|
+
private readonly model;
|
|
10
|
+
private readonly dims;
|
|
11
|
+
private readonly vectors;
|
|
12
|
+
private dirty;
|
|
13
|
+
private constructor();
|
|
14
|
+
static open(directory: string, model: string, dims: number): VectorCache;
|
|
15
|
+
private read;
|
|
16
|
+
/** The vector of each entity, computing only the ones not cached. */
|
|
17
|
+
vectorsFor(entities: readonly SearchHintsEntity[], encode: (text: string) => Float32Array): Float32Array[];
|
|
18
|
+
/** Keep only the given entities, so deleted ones do not grow the file. */
|
|
19
|
+
retain(entities: readonly SearchHintsEntity[]): void;
|
|
20
|
+
/** Cached entity keys in file order; for inspection and tests. */
|
|
21
|
+
keys(): string[];
|
|
22
|
+
/** Write atomically: a reader sees the old file or the new one. */
|
|
23
|
+
save(): void;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAezD;;;;GAIG;AAEH,qBAAa,WAAW;IAKpB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,IAAI;IANvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;IAC3D,OAAO,CAAC,KAAK,CAAS;IAEtB,OAAO;IAMP,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW;IAMxE,OAAO,CAAC,IAAI;IA6BZ,qEAAqE;IACrE,UAAU,CACR,QAAQ,EAAE,SAAS,iBAAiB,EAAE,EACtC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,YAAY,GACrC,YAAY,EAAE;IAajB,0EAA0E;IAC1E,MAAM,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,GAAG,IAAI;IASpD,kEAAkE;IAClE,IAAI,IAAI,MAAM,EAAE;IAIhB,mEAAmE;IACnE,IAAI,IAAI,IAAI;CAgCb"}
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const MAGIC = "KSHC";
|
|
4
|
+
const FORMAT_VERSION = 1;
|
|
5
|
+
const FILE_NAME = "vectors.bin";
|
|
6
|
+
/**
|
|
7
|
+
* Entity vectors keyed by type::id and the host content hash. A changed
|
|
8
|
+
* entity, a different model or a damaged file means that vector (or the
|
|
9
|
+
* whole file) is rebuilt; nothing stale is ever returned.
|
|
10
|
+
*/
|
|
11
|
+
// implements REQ-kibi-plugin-search-hints
|
|
12
|
+
export class VectorCache {
|
|
13
|
+
file;
|
|
14
|
+
model;
|
|
15
|
+
dims;
|
|
16
|
+
vectors = new Map();
|
|
17
|
+
dirty = false;
|
|
18
|
+
constructor(file, model, dims) {
|
|
19
|
+
this.file = file;
|
|
20
|
+
this.model = model;
|
|
21
|
+
this.dims = dims;
|
|
22
|
+
}
|
|
23
|
+
static open(directory, model, dims) {
|
|
24
|
+
const cache = new VectorCache(path.join(directory, FILE_NAME), model, dims);
|
|
25
|
+
cache.read();
|
|
26
|
+
return cache;
|
|
27
|
+
}
|
|
28
|
+
read() {
|
|
29
|
+
// A missing, foreign or damaged file leaves the cache empty: every
|
|
30
|
+
// vector is then rebuilt, and nothing stale is ever returned.
|
|
31
|
+
try {
|
|
32
|
+
const bytes = readFileSync(this.file);
|
|
33
|
+
if (bytes.toString("ascii", 0, 4) !== MAGIC ||
|
|
34
|
+
bytes.readUInt32LE(4) !== FORMAT_VERSION)
|
|
35
|
+
return;
|
|
36
|
+
const headerLength = bytes.readUInt32LE(8);
|
|
37
|
+
const header = JSON.parse(bytes.toString("utf8", 12, 12 + headerLength));
|
|
38
|
+
// A file of other dimensions fails the length check below.
|
|
39
|
+
if (header.model !== this.model)
|
|
40
|
+
return;
|
|
41
|
+
const start = 12 + headerLength;
|
|
42
|
+
const width = this.dims * 4;
|
|
43
|
+
if (bytes.length !== start + header.entries.length * width)
|
|
44
|
+
return;
|
|
45
|
+
header.entries.forEach(([key, hash], index) => {
|
|
46
|
+
const offset = start + index * width;
|
|
47
|
+
const vector = Float32Array.from({ length: this.dims }, (_, col) => bytes.readFloatLE(offset + col * 4));
|
|
48
|
+
this.vectors.set(key, { hash, vector });
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch { }
|
|
52
|
+
}
|
|
53
|
+
/** The vector of each entity, computing only the ones not cached. */
|
|
54
|
+
vectorsFor(entities, encode) {
|
|
55
|
+
return entities.map((entity) => {
|
|
56
|
+
const key = `${entity.type}::${entity.id}`;
|
|
57
|
+
const cached = this.vectors.get(key);
|
|
58
|
+
if (cached !== undefined && cached.hash === entity.hash)
|
|
59
|
+
return cached.vector;
|
|
60
|
+
const vector = encode(entity.text);
|
|
61
|
+
this.vectors.set(key, { hash: entity.hash, vector });
|
|
62
|
+
this.dirty = true;
|
|
63
|
+
return vector;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/** Keep only the given entities, so deleted ones do not grow the file. */
|
|
67
|
+
retain(entities) {
|
|
68
|
+
const keep = new Set(entities.map(cacheKey));
|
|
69
|
+
for (const key of this.vectors.keys())
|
|
70
|
+
if (!keep.has(key)) {
|
|
71
|
+
this.vectors.delete(key);
|
|
72
|
+
this.dirty = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Cached entity keys in file order; for inspection and tests. */
|
|
76
|
+
keys() {
|
|
77
|
+
return [...this.vectors.keys()].sort();
|
|
78
|
+
}
|
|
79
|
+
/** Write atomically: a reader sees the old file or the new one. */
|
|
80
|
+
save() {
|
|
81
|
+
if (!this.dirty)
|
|
82
|
+
return;
|
|
83
|
+
const entries = this.keys().map((key) => [key, this.vectors.get(key)]);
|
|
84
|
+
const header = Buffer.from(JSON.stringify({
|
|
85
|
+
model: this.model,
|
|
86
|
+
dims: this.dims,
|
|
87
|
+
entries: entries.map(([key, { hash }]) => [key, hash]),
|
|
88
|
+
}), "utf8");
|
|
89
|
+
const width = this.dims * 4;
|
|
90
|
+
const out = Buffer.alloc(12 + header.length + entries.length * width);
|
|
91
|
+
out.write(MAGIC, 0);
|
|
92
|
+
out.writeUInt32LE(FORMAT_VERSION, 4);
|
|
93
|
+
out.writeUInt32LE(header.length, 8);
|
|
94
|
+
header.copy(out, 12);
|
|
95
|
+
entries.forEach(([, { vector }], index) => {
|
|
96
|
+
const offset = 12 + header.length + index * width;
|
|
97
|
+
vector.forEach((value, col) => out.writeFloatLE(value, offset + col * 4));
|
|
98
|
+
});
|
|
99
|
+
const temporary = `${this.file}.${process.pid}.tmp`;
|
|
100
|
+
try {
|
|
101
|
+
writeFileSync(temporary, out, { mode: 0o600 });
|
|
102
|
+
renameSync(temporary, this.file);
|
|
103
|
+
this.dirty = false;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// An unwritable cache only costs a rebuild next time.
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function cacheKey(entity) {
|
|
111
|
+
return `${entity.type}::${entity.id}`;
|
|
112
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type SearchHint, type SearchHintsInput, type SearchHintsV1 } from "kibi-plugin-sdk";
|
|
2
|
+
import { VectorCache } from "./cache.js";
|
|
3
|
+
import { type StaticModel } from "./model.js";
|
|
4
|
+
export { VectorCache } from "./cache.js";
|
|
5
|
+
export { MODEL_ID, MODEL_LABEL, MODEL_REVISION, loadStaticModel, } from "./model.js";
|
|
6
|
+
export type { StaticModel } from "./model.js";
|
|
7
|
+
export { WordPieceTokenizer, normalize, preTokenize } from "./tokenizer.js";
|
|
8
|
+
/**
|
|
9
|
+
* Rank the supplied entities by cosine similarity to the query. Scores are
|
|
10
|
+
* cosine clamped to [0, 1]; ties are ordered by id, so the same input always
|
|
11
|
+
* gives the same hints.
|
|
12
|
+
*/
|
|
13
|
+
export declare function rankByMeaning(staticModel: StaticModel, cache: VectorCache, input: SearchHintsInput): SearchHint[];
|
|
14
|
+
export declare function createSearchHints(): SearchHintsV1;
|
|
15
|
+
/**
|
|
16
|
+
* Offline meaning-based hints for weak kb_search answers, from the bundled
|
|
17
|
+
* potion-base-8M static embedding. No network, no native code.
|
|
18
|
+
*/
|
|
19
|
+
export declare const kibiPlugin: {
|
|
20
|
+
apiVersion: "kibi.plugin.v1";
|
|
21
|
+
id: string;
|
|
22
|
+
version: string;
|
|
23
|
+
permissions: {
|
|
24
|
+
network: false;
|
|
25
|
+
metered: false;
|
|
26
|
+
secrets: never[];
|
|
27
|
+
};
|
|
28
|
+
capabilities: {
|
|
29
|
+
searchHints: SearchHintsV1;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
export default kibiPlugin;
|
|
33
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,gBAAgB,EAErB,KAAK,aAAa,EAEnB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAe,KAAK,WAAW,EAAmB,MAAM,YAAY,CAAC;AAE5E,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EACL,QAAQ,EACR,WAAW,EACX,cAAc,EACd,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAkB5E;;;;GAIG;AAEH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,WAAW,EAClB,KAAK,EAAE,gBAAgB,GACtB,UAAU,EAAE,CA0Bd;AAGD,wBAAgB,iBAAiB,IAAI,aAAa,CAoBjD;AAED;;;GAGG;AAEH,eAAO,MAAM,UAAU;;;;;;;;;;;;CAMrB,CAAC;AAEH,eAAe,UAAU,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { KIBI_PLUGIN_API_VERSION, defineKibiPlugin, } from "kibi-plugin-sdk";
|
|
3
|
+
import { VectorCache } from "./cache.js";
|
|
4
|
+
import { MODEL_LABEL, loadStaticModel } from "./model.js";
|
|
5
|
+
export { VectorCache } from "./cache.js";
|
|
6
|
+
export { MODEL_ID, MODEL_LABEL, MODEL_REVISION, loadStaticModel, } from "./model.js";
|
|
7
|
+
export { WordPieceTokenizer, normalize, preTokenize } from "./tokenizer.js";
|
|
8
|
+
const packageJson = createRequire(import.meta.url)("../package.json");
|
|
9
|
+
let model;
|
|
10
|
+
const caches = new Map();
|
|
11
|
+
/** kibi.search-hints.v1 caps a request at 20 hints. */
|
|
12
|
+
const MAX_LIMIT = 20;
|
|
13
|
+
function dot(left, right) {
|
|
14
|
+
return left.reduce((sum, value, index) => sum + value * right[index], 0);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Rank the supplied entities by cosine similarity to the query. Scores are
|
|
18
|
+
* cosine clamped to [0, 1]; ties are ordered by id, so the same input always
|
|
19
|
+
* gives the same hints.
|
|
20
|
+
*/
|
|
21
|
+
// implements REQ-kibi-plugin-search-hints
|
|
22
|
+
export function rankByMeaning(staticModel, cache, input) {
|
|
23
|
+
const vectors = cache.vectorsFor(input.entities, (text) => staticModel.encode(text));
|
|
24
|
+
cache.retain(input.entities);
|
|
25
|
+
cache.save();
|
|
26
|
+
const query = staticModel.encode(input.query);
|
|
27
|
+
const limit = Math.max(0, Math.min(input.limit, MAX_LIMIT));
|
|
28
|
+
return input.entities
|
|
29
|
+
.map((entity, index) => ({
|
|
30
|
+
id: entity.id,
|
|
31
|
+
score: Math.min(1, Math.max(0, dot(query, vectors[index]))),
|
|
32
|
+
}))
|
|
33
|
+
.filter((hint) => hint.score > 0)
|
|
34
|
+
.sort((left, right) => right.score - left.score ||
|
|
35
|
+
// rationale: the host sends each id once, so ids never tie and < and
|
|
36
|
+
// <= order alike.
|
|
37
|
+
// Stryker disable next-line EqualityOperator
|
|
38
|
+
(left.id < right.id ? -1 : 1))
|
|
39
|
+
.slice(0, limit);
|
|
40
|
+
}
|
|
41
|
+
// implements REQ-kibi-plugin-search-hints
|
|
42
|
+
export function createSearchHints() {
|
|
43
|
+
return {
|
|
44
|
+
id: "kibi-plugin-search-hints.potion-base-8M",
|
|
45
|
+
model: MODEL_LABEL,
|
|
46
|
+
hint(input) {
|
|
47
|
+
// The table loads on the first hint, not when the plugin is resolved.
|
|
48
|
+
model ??= loadStaticModel();
|
|
49
|
+
const staticModel = model;
|
|
50
|
+
let cache = caches.get(input.cacheDirectory);
|
|
51
|
+
if (cache === undefined) {
|
|
52
|
+
cache = VectorCache.open(input.cacheDirectory, staticModel.id, staticModel.dims);
|
|
53
|
+
caches.set(input.cacheDirectory, cache);
|
|
54
|
+
}
|
|
55
|
+
return { hints: rankByMeaning(staticModel, cache, input) };
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Offline meaning-based hints for weak kb_search answers, from the bundled
|
|
61
|
+
* potion-base-8M static embedding. No network, no native code.
|
|
62
|
+
*/
|
|
63
|
+
// implements REQ-kibi-plugin-search-hints
|
|
64
|
+
export const kibiPlugin = defineKibiPlugin({
|
|
65
|
+
apiVersion: KIBI_PLUGIN_API_VERSION,
|
|
66
|
+
id: "kibi-plugin-search-hints",
|
|
67
|
+
version: packageJson.version,
|
|
68
|
+
permissions: { network: false, metered: false, secrets: [] },
|
|
69
|
+
capabilities: { searchHints: createSearchHints() },
|
|
70
|
+
});
|
|
71
|
+
export default kibiPlugin;
|
package/dist/model.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Upstream model and the revision the bundled asset was built from. */
|
|
2
|
+
export declare const MODEL_ID = "minishlab/potion-base-8M";
|
|
3
|
+
export declare const MODEL_REVISION = "bf8b056651a2c21b8d2565580b8569da283cab23";
|
|
4
|
+
/** The model a provider stamp discloses. */
|
|
5
|
+
export declare const MODEL_LABEL: string;
|
|
6
|
+
export type StaticModel = Readonly<{
|
|
7
|
+
/** Model id plus the asset hash, so a changed table never reuses vectors. */
|
|
8
|
+
id: string;
|
|
9
|
+
dims: number;
|
|
10
|
+
encode(text: string): Float32Array;
|
|
11
|
+
}>;
|
|
12
|
+
/**
|
|
13
|
+
* A static embedding: the mean of the int8 table rows of a text's tokens,
|
|
14
|
+
* L2-normalised. Sums run in double precision in token order, so a text has
|
|
15
|
+
* the same vector on every platform and in every batch.
|
|
16
|
+
*/
|
|
17
|
+
export declare function loadStaticModel(assetsDirectory?: string): StaticModel;
|
|
18
|
+
//# sourceMappingURL=model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAOA,wEAAwE;AACxE,eAAO,MAAM,QAAQ,6BAA6B,CAAC;AACnD,eAAO,MAAM,cAAc,6CAA6C,CAAC;AACzE,4CAA4C;AAC5C,eAAO,MAAM,WAAW,QAAoD,CAAC;AAK7E,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC;IACjC,6EAA6E;IAC7E,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAAC;CACpC,CAAC,CAAC;AAIH;;;;GAIG;AAEH,wBAAgB,eAAe,CAC7B,eAAe,SAAmB,GACjC,WAAW,CAmDb"}
|
package/dist/model.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { WordPieceTokenizer } from "./tokenizer.js";
|
|
6
|
+
/** Upstream model and the revision the bundled asset was built from. */
|
|
7
|
+
export const MODEL_ID = "minishlab/potion-base-8M";
|
|
8
|
+
export const MODEL_REVISION = "bf8b056651a2c21b8d2565580b8569da283cab23";
|
|
9
|
+
/** The model a provider stamp discloses. */
|
|
10
|
+
export const MODEL_LABEL = `${MODEL_ID}@${MODEL_REVISION.slice(0, 12)}/int8`;
|
|
11
|
+
/** model2vec truncates each text to this many tokens. */
|
|
12
|
+
const MAX_TOKENS = 512;
|
|
13
|
+
const ASSETS_DIRECTORY = fileURLToPath(new URL("../assets/", import.meta.url));
|
|
14
|
+
/**
|
|
15
|
+
* A static embedding: the mean of the int8 table rows of a text's tokens,
|
|
16
|
+
* L2-normalised. Sums run in double precision in token order, so a text has
|
|
17
|
+
* the same vector on every platform and in every batch.
|
|
18
|
+
*/
|
|
19
|
+
// implements REQ-kibi-plugin-search-hints
|
|
20
|
+
export function loadStaticModel(assetsDirectory = ASSETS_DIRECTORY) {
|
|
21
|
+
const table = readFileSync(path.join(assetsDirectory, "potion-base-8M.int8.bin"));
|
|
22
|
+
if (table.toString("ascii", 0, 4) !== "KSHM" || table.readUInt32LE(4) !== 1)
|
|
23
|
+
throw new Error("search hints model asset has an unknown format");
|
|
24
|
+
const rows = table.readUInt32LE(8);
|
|
25
|
+
const dims = table.readUInt32LE(12);
|
|
26
|
+
const scales = Float64Array.from({ length: rows }, (_, row) => table.readFloatLE(16 + row * 4));
|
|
27
|
+
const weights = new Int8Array(table.buffer, table.byteOffset + 16 + rows * 4, rows * dims);
|
|
28
|
+
const vocabulary = readFileSync(path.join(assetsDirectory, "vocab.txt"), "utf8")
|
|
29
|
+
.trimEnd()
|
|
30
|
+
.split("\n");
|
|
31
|
+
if (vocabulary.length !== rows)
|
|
32
|
+
throw new Error(`search hints vocabulary has ${vocabulary.length} tokens for ${rows} table rows`);
|
|
33
|
+
const tokenizer = new WordPieceTokenizer(vocabulary);
|
|
34
|
+
const digest = createHash("sha256").update(table).digest("hex");
|
|
35
|
+
return {
|
|
36
|
+
id: `${MODEL_LABEL}-${digest.slice(0, 12)}`,
|
|
37
|
+
dims,
|
|
38
|
+
encode(text) {
|
|
39
|
+
const ids = tokenizer
|
|
40
|
+
.encode(text)
|
|
41
|
+
.filter((token) => token !== tokenizer.unkId)
|
|
42
|
+
.slice(0, MAX_TOKENS);
|
|
43
|
+
const sum = new Float64Array(dims);
|
|
44
|
+
for (const token of ids) {
|
|
45
|
+
const scale = scales[token];
|
|
46
|
+
weights
|
|
47
|
+
.subarray(token * dims, (token + 1) * dims)
|
|
48
|
+
.forEach((weight, col) => {
|
|
49
|
+
sum[col] = sum[col] + weight * scale;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const norm = Math.sqrt(sum.reduce((total, value) => total + value * value, 0));
|
|
53
|
+
return Float32Array.from(sum, (value) => (norm === 0 ? 0 : value / norm));
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BERT WordPiece tokenizer, matching the Hugging Face `tokenizers`
|
|
3
|
+
* BertNormalizer (clean text, CJK spacing, accent stripping, lowercase),
|
|
4
|
+
* BertPreTokenizer and WordPiece model that potion-base-8M ships with.
|
|
5
|
+
*/
|
|
6
|
+
export declare const UNK_TOKEN = "[UNK]";
|
|
7
|
+
export declare function normalize(text: string): string;
|
|
8
|
+
export declare function preTokenize(normalized: string): string[];
|
|
9
|
+
export declare class WordPieceTokenizer {
|
|
10
|
+
private readonly ids;
|
|
11
|
+
readonly unkId: number;
|
|
12
|
+
constructor(vocabulary: readonly string[]);
|
|
13
|
+
private word;
|
|
14
|
+
/** Token ids without special tokens, as `add_special_tokens: false`. */
|
|
15
|
+
encode(text: string): number[];
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=tokenizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokenizer.d.ts","sourceRoot":"","sources":["../src/tokenizer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,eAAO,MAAM,SAAS,UAAU,CAAC;AAqCjC,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAW9C;AAGD,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAiBxD;AAGD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA8B;IAClD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;gBAEX,UAAU,EAAE,SAAS,MAAM,EAAE;IAOzC,OAAO,CAAC,IAAI;IA8BZ,wEAAwE;IACxE,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE;CAY/B"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BERT WordPiece tokenizer, matching the Hugging Face `tokenizers`
|
|
3
|
+
* BertNormalizer (clean text, CJK spacing, accent stripping, lowercase),
|
|
4
|
+
* BertPreTokenizer and WordPiece model that potion-base-8M ships with.
|
|
5
|
+
*/
|
|
6
|
+
const MAX_INPUT_CHARS_PER_WORD = 100;
|
|
7
|
+
const CONTINUING_PREFIX = "##";
|
|
8
|
+
export const UNK_TOKEN = "[UNK]";
|
|
9
|
+
/** Added tokens matched verbatim before normalization, as tokenizer.json declares. */
|
|
10
|
+
const SPECIAL_TOKENS = ["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"];
|
|
11
|
+
const SPECIAL_SPLIT = /(\[PAD\]|\[UNK\]|\[CLS\]|\[SEP\]|\[MASK\])/;
|
|
12
|
+
// Unassigned code points (Cn) are kept, as the upstream normalizer does.
|
|
13
|
+
const CONTROL = /[\p{Cc}\p{Cf}\p{Co}]/u;
|
|
14
|
+
const CLEAN_WHITESPACE = /[\t\n\r\p{Zs}]/u;
|
|
15
|
+
const WHITESPACE = /\p{White_Space}/u;
|
|
16
|
+
// BERT also counts every ASCII symbol as punctuation, not only Unicode P.
|
|
17
|
+
const PUNCTUATION = /[\p{P}$+<=>^`|~]/u;
|
|
18
|
+
const NONSPACING_MARK = /\p{Mn}/gu;
|
|
19
|
+
function isControl(char) {
|
|
20
|
+
if (char === "\t" || char === "\n" || char === "\r")
|
|
21
|
+
return false;
|
|
22
|
+
return CONTROL.test(char);
|
|
23
|
+
}
|
|
24
|
+
function isChineseChar(code) {
|
|
25
|
+
return ((code >= 0x4e00 && code <= 0x9fff) ||
|
|
26
|
+
(code >= 0x3400 && code <= 0x4dbf) ||
|
|
27
|
+
(code >= 0x20000 && code <= 0x2a6df) ||
|
|
28
|
+
(code >= 0x2a700 && code <= 0x2b73f) ||
|
|
29
|
+
(code >= 0x2b740 && code <= 0x2b81f) ||
|
|
30
|
+
// Upstream starts this range at 0x2B920, not Unicode's 0x2B820.
|
|
31
|
+
(code >= 0x2b920 && code <= 0x2ceaf) ||
|
|
32
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
33
|
+
(code >= 0x2f800 && code <= 0x2fa1f));
|
|
34
|
+
}
|
|
35
|
+
function isPunctuation(char) {
|
|
36
|
+
return PUNCTUATION.test(char);
|
|
37
|
+
}
|
|
38
|
+
// implements REQ-kibi-plugin-search-hints
|
|
39
|
+
export function normalize(text) {
|
|
40
|
+
let cleaned = "";
|
|
41
|
+
for (const char of text) {
|
|
42
|
+
const code = char.codePointAt(0) ?? 0;
|
|
43
|
+
// NUL is a control character too.
|
|
44
|
+
if (code === 0xfffd || isControl(char))
|
|
45
|
+
continue;
|
|
46
|
+
if (CLEAN_WHITESPACE.test(char))
|
|
47
|
+
cleaned += " ";
|
|
48
|
+
else if (isChineseChar(code))
|
|
49
|
+
cleaned += ` ${char} `;
|
|
50
|
+
else
|
|
51
|
+
cleaned += char;
|
|
52
|
+
}
|
|
53
|
+
return cleaned.normalize("NFD").replace(NONSPACING_MARK, "").toLowerCase();
|
|
54
|
+
}
|
|
55
|
+
// implements REQ-kibi-plugin-search-hints
|
|
56
|
+
export function preTokenize(normalized) {
|
|
57
|
+
const words = [];
|
|
58
|
+
let current = "";
|
|
59
|
+
for (const char of normalized) {
|
|
60
|
+
if (WHITESPACE.test(char)) {
|
|
61
|
+
if (current !== "")
|
|
62
|
+
words.push(current);
|
|
63
|
+
current = "";
|
|
64
|
+
}
|
|
65
|
+
else if (isPunctuation(char)) {
|
|
66
|
+
if (current !== "")
|
|
67
|
+
words.push(current);
|
|
68
|
+
words.push(char);
|
|
69
|
+
current = "";
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
current += char;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (current !== "")
|
|
76
|
+
words.push(current);
|
|
77
|
+
return words;
|
|
78
|
+
}
|
|
79
|
+
// implements REQ-kibi-plugin-search-hints
|
|
80
|
+
export class WordPieceTokenizer {
|
|
81
|
+
ids;
|
|
82
|
+
unkId;
|
|
83
|
+
constructor(vocabulary) {
|
|
84
|
+
this.ids = new Map(vocabulary.map((token, id) => [token, id]));
|
|
85
|
+
const unk = this.ids.get(UNK_TOKEN);
|
|
86
|
+
if (unk === undefined)
|
|
87
|
+
throw new Error("vocabulary has no [UNK] token");
|
|
88
|
+
this.unkId = unk;
|
|
89
|
+
}
|
|
90
|
+
word(word, out) {
|
|
91
|
+
const chars = Array.from(word);
|
|
92
|
+
if (chars.length > MAX_INPUT_CHARS_PER_WORD) {
|
|
93
|
+
out.push(this.unkId);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const pieces = [];
|
|
97
|
+
let start = 0;
|
|
98
|
+
while (start < chars.length) {
|
|
99
|
+
let end = chars.length;
|
|
100
|
+
let found;
|
|
101
|
+
// rationale: at start === end the piece is empty, and neither "" nor
|
|
102
|
+
// "##" is a vocabulary entry, so the loop ends the same way.
|
|
103
|
+
// Stryker disable next-line EqualityOperator
|
|
104
|
+
while (start < end) {
|
|
105
|
+
const piece = chars.slice(start, end).join("");
|
|
106
|
+
found = this.ids.get(start > 0 ? CONTINUING_PREFIX + piece : piece);
|
|
107
|
+
if (found !== undefined)
|
|
108
|
+
break;
|
|
109
|
+
end--;
|
|
110
|
+
}
|
|
111
|
+
if (found === undefined) {
|
|
112
|
+
out.push(this.unkId);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
pieces.push(found);
|
|
116
|
+
start = end;
|
|
117
|
+
}
|
|
118
|
+
out.push(...pieces);
|
|
119
|
+
}
|
|
120
|
+
/** Token ids without special tokens, as `add_special_tokens: false`. */
|
|
121
|
+
encode(text) {
|
|
122
|
+
const out = [];
|
|
123
|
+
for (const part of text.split(SPECIAL_SPLIT)) {
|
|
124
|
+
const special = SPECIAL_TOKENS.includes(part)
|
|
125
|
+
? this.ids.get(part)
|
|
126
|
+
: undefined;
|
|
127
|
+
if (special !== undefined)
|
|
128
|
+
out.push(special);
|
|
129
|
+
else
|
|
130
|
+
for (const word of preTokenize(normalize(part)))
|
|
131
|
+
this.word(word, out);
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
}
|
package/integrity.json
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "kibi.package-integrity.v1",
|
|
3
|
+
"package": "kibi-plugin-search-hints",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"algorithm": "sha256",
|
|
6
|
+
"selfHash": {
|
|
7
|
+
"path": "integrity.json",
|
|
8
|
+
"excluded": true,
|
|
9
|
+
"reason": "A manifest cannot contain its own digest; the host approval digest covers this file."
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "CHANGELOG.md",
|
|
14
|
+
"bytes": 1902,
|
|
15
|
+
"sha256": "0fae42f30add91b196f8209a207b912973cd5d586904bf56ed348b1c8e033cc2"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "LICENSE.md",
|
|
19
|
+
"bytes": 34519,
|
|
20
|
+
"sha256": "068de0a1cda6e78feb18db2150be2f023dca2b157062257b0fb8de50a359b22c"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"path": "README.md",
|
|
24
|
+
"bytes": 1474,
|
|
25
|
+
"sha256": "0607469f6a6a35aa3eb6a5eecf7d7ebe4663f1f707dd8dcd8c292183372955e8"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"path": "SBOM.spdx.json",
|
|
29
|
+
"bytes": 2353,
|
|
30
|
+
"sha256": "236722d2eaf390065ab6f1bfba872610d99d48346ce635703a85add0a26bdedc"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"path": "THIRD_PARTY_NOTICES.md",
|
|
34
|
+
"bytes": 993,
|
|
35
|
+
"sha256": "3b0b169c9173957364e4b03aeab1ebe87efd488fb3b465c022027ce7adc8c331"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"path": "assets/potion-base-8M.int8.bin",
|
|
39
|
+
"bytes": 7677296,
|
|
40
|
+
"sha256": "02eb3b486a152a01c49055e109cce3e483fc3b49761f5343b5518c0cfc76f54f"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"path": "assets/vocab.txt",
|
|
44
|
+
"bytes": 219690,
|
|
45
|
+
"sha256": "1394523a67ddd404a825428018c0582a6998bcfa044ecbcbf1f4d71adb94c61c"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"path": "catalog.json",
|
|
49
|
+
"bytes": 900,
|
|
50
|
+
"sha256": "686f031bfbffe8593e6dc355d3a859c067e17f3f61376abfe4dc84d11b48f4a1"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"path": "dist/cache.d.ts",
|
|
54
|
+
"bytes": 1103,
|
|
55
|
+
"sha256": "adeca3b210197f3f1039eb82024d800e806744a2e248b0d193de3ac4e0faf749"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"path": "dist/cache.d.ts.map",
|
|
59
|
+
"bytes": 723,
|
|
60
|
+
"sha256": "e7fea8c72ed8220c1a029509c865dca37026fc15f3e112237b7f48dca6b2ad43"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"path": "dist/cache.js",
|
|
64
|
+
"bytes": 4326,
|
|
65
|
+
"sha256": "5c35a4e265c8a48a1adb573770e97ea1a0126e4fc4dd23d7f508f52cce29f450"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"path": "dist/index.d.ts",
|
|
69
|
+
"bytes": 1288,
|
|
70
|
+
"sha256": "73eb11ee7415166394fd4df7cee6c53b9e12f0e149b2681721a28a36053c33ec"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"path": "dist/index.d.ts.map",
|
|
74
|
+
"bytes": 720,
|
|
75
|
+
"sha256": "b36616a0cf872a9de7aec8660dbdae5d3b12b9a36ecc07b2d1c250188e6c8d4f"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"path": "dist/index.js",
|
|
79
|
+
"bytes": 2978,
|
|
80
|
+
"sha256": "6a5eff91a9796e7fcc24ad3797d83840119894909f9a34a1858a3892b99ac8f9"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"path": "dist/model.d.ts",
|
|
84
|
+
"bytes": 831,
|
|
85
|
+
"sha256": "a9bc35f94df9392d8bb7d3167ee6bc17da6a2176532735132f20704e78039ddc"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"path": "dist/model.d.ts.map",
|
|
89
|
+
"bytes": 451,
|
|
90
|
+
"sha256": "e36bf801454b83d4d76902dfd903f7019e7edc74a5b050e232bc299d1036f8fd"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"path": "dist/model.js",
|
|
94
|
+
"bytes": 2765,
|
|
95
|
+
"sha256": "13af1dc20ce42d374264c14953d40320c2da7b23bf413db4af993bf928251621"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"path": "dist/tokenizer.d.ts",
|
|
99
|
+
"bytes": 704,
|
|
100
|
+
"sha256": "d949a9244fae4f6a29d53d3a75c554d517c3b3fe684028d52ef26d3b0981ab21"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"path": "dist/tokenizer.d.ts.map",
|
|
104
|
+
"bytes": 476,
|
|
105
|
+
"sha256": "e44d6f2e0b5e626d8e87e9fc9747e7c1d5bba44f36909ca15a2e69950d516c0c"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"path": "dist/tokenizer.js",
|
|
109
|
+
"bytes": 4728,
|
|
110
|
+
"sha256": "e35b1200ada6df138c36d90a7c861242a1b7eb3a662dadf653bf102495a59cc8"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"path": "licenses/bge-base-en-v1.5-vocabulary-MIT.txt",
|
|
114
|
+
"bytes": 1065,
|
|
115
|
+
"sha256": "587a673933425dbc36ec61268d3b954051b2d3ef3c9b322ede357976055ffdd5"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"path": "licenses/potion-base-8M-MIT.txt",
|
|
119
|
+
"bytes": 1074,
|
|
120
|
+
"sha256": "b0214f148eceae739f916209e78cfe15712c4b810c8bc9d44b6c2d1d1616aab6"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"path": "package.json",
|
|
124
|
+
"bytes": 1320,
|
|
125
|
+
"sha256": "0963d85f6b5483292d0e0e7e3ce313c2790e860ac93506f555f29a65d86058d4"
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 staoxiao
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Thomas van Dongen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|