knolo-core 0.2.2 → 0.3.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/DOCS.md +228 -14
- package/LICENSE +180 -2
- package/README.md +237 -148
- package/bin/knolo.mjs +120 -13
- package/dist/builder.d.ts +13 -1
- package/dist/builder.js +103 -11
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -2
- package/dist/indexer.d.ts +2 -1
- package/dist/indexer.js +3 -2
- package/dist/pack.d.ts +12 -0
- package/dist/pack.js +77 -4
- package/dist/patch.d.ts +1 -8
- package/dist/patch.js +2 -17
- package/dist/query.d.ts +28 -0
- package/dist/query.js +282 -18
- package/dist/rank.d.ts +1 -1
- package/dist/rank.js +5 -4
- package/dist/semantic.d.ts +7 -0
- package/dist/semantic.js +98 -0
- package/dist/tokenize.js +1 -1
- package/package.json +3 -2
package/bin/knolo.mjs
CHANGED
|
@@ -10,12 +10,10 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
10
10
|
const require = createRequire(import.meta.url);
|
|
11
11
|
|
|
12
12
|
async function tryImport(filePath) {
|
|
13
|
-
// 1) ESM via file URL
|
|
14
13
|
try {
|
|
15
14
|
const url = pathToFileURL(filePath).href;
|
|
16
15
|
return await import(url);
|
|
17
16
|
} catch (_) {}
|
|
18
|
-
// 2) CJS via require
|
|
19
17
|
try {
|
|
20
18
|
return require(filePath);
|
|
21
19
|
} catch (_) {}
|
|
@@ -24,14 +22,11 @@ async function tryImport(filePath) {
|
|
|
24
22
|
|
|
25
23
|
function getBuildPack(mod) {
|
|
26
24
|
if (!mod) return undefined;
|
|
27
|
-
// Named export (ESM)
|
|
28
25
|
if (typeof mod.buildPack === "function") return mod.buildPack;
|
|
29
|
-
// CJS default export object: { buildPack } or function
|
|
30
26
|
if (mod.default) {
|
|
31
27
|
if (typeof mod.default === "function") return mod.default;
|
|
32
28
|
if (typeof mod.default.buildPack === "function") return mod.default.buildPack;
|
|
33
29
|
}
|
|
34
|
-
// Some CJS setups export { buildPack } directly
|
|
35
30
|
if (typeof mod === "function") return mod;
|
|
36
31
|
if (typeof mod.buildPack === "function") return mod.buildPack;
|
|
37
32
|
return undefined;
|
|
@@ -41,7 +36,6 @@ async function loadBuildPack() {
|
|
|
41
36
|
const candidates = [
|
|
42
37
|
path.resolve(__dirname, "../dist/index.js"),
|
|
43
38
|
path.resolve(__dirname, "../dist/builder.js"),
|
|
44
|
-
// Also try .cjs just in case someone built CJS
|
|
45
39
|
path.resolve(__dirname, "../dist/index.cjs"),
|
|
46
40
|
path.resolve(__dirname, "../dist/builder.cjs"),
|
|
47
41
|
];
|
|
@@ -53,17 +47,130 @@ async function loadBuildPack() {
|
|
|
53
47
|
throw new Error("Could not locate a buildPack function in dist/");
|
|
54
48
|
}
|
|
55
49
|
|
|
50
|
+
function validateCliDocs(raw) {
|
|
51
|
+
if (!Array.isArray(raw)) {
|
|
52
|
+
throw new Error('Input JSON must be an array of docs: [{ "text": "...", "id"?: "...", "heading"?: "..." }]');
|
|
53
|
+
}
|
|
54
|
+
for (let i = 0; i < raw.length; i++) {
|
|
55
|
+
const doc = raw[i];
|
|
56
|
+
if (!doc || typeof doc !== "object") {
|
|
57
|
+
throw new Error(`Invalid doc at index ${i}: expected an object.`);
|
|
58
|
+
}
|
|
59
|
+
if (typeof doc.text !== "string" || !doc.text.trim()) {
|
|
60
|
+
throw new Error(`Invalid doc at index ${i}: "text" must be a non-empty string.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return raw;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseArgs(argv) {
|
|
67
|
+
const positional = [];
|
|
68
|
+
const flags = { embeddingsPath: undefined, modelId: undefined };
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < argv.length; i++) {
|
|
71
|
+
const arg = argv[i];
|
|
72
|
+
if (!arg.startsWith("--")) {
|
|
73
|
+
positional.push(arg);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (arg === "--embeddings") {
|
|
77
|
+
flags.embeddingsPath = argv[++i];
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (arg === "--model-id") {
|
|
81
|
+
flags.modelId = argv[++i];
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (arg === "--help" || arg === "-h") {
|
|
85
|
+
flags.help = true;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`Unknown flag: ${arg}`);
|
|
89
|
+
}
|
|
90
|
+
return { positional, flags };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function loadEmbeddingsFromJson(filePath, expectedCount) {
|
|
94
|
+
const parsed = JSON.parse(readFileSync(filePath, "utf8"));
|
|
95
|
+
const vectors = Array.isArray(parsed?.embeddings) ? parsed.embeddings : parsed;
|
|
96
|
+
if (!Array.isArray(vectors)) {
|
|
97
|
+
throw new Error('Embeddings JSON must be either an array of vectors or { "embeddings": [...] }.');
|
|
98
|
+
}
|
|
99
|
+
if (vectors.length !== expectedCount) {
|
|
100
|
+
throw new Error(`Embeddings length mismatch: expected ${expectedCount}, got ${vectors.length}.`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const first = vectors[0];
|
|
104
|
+
if (!Array.isArray(first) || first.length === 0) {
|
|
105
|
+
throw new Error("Embeddings must contain non-empty numeric vectors.");
|
|
106
|
+
}
|
|
107
|
+
const dims = first.length;
|
|
108
|
+
|
|
109
|
+
return vectors.map((entry, i) => {
|
|
110
|
+
if (!Array.isArray(entry)) {
|
|
111
|
+
throw new Error(`Embeddings[${i}] must be an array of numbers.`);
|
|
112
|
+
}
|
|
113
|
+
if (entry.length !== dims) {
|
|
114
|
+
throw new Error(`Embeddings[${i}] has dims ${entry.length}, expected ${dims}.`);
|
|
115
|
+
}
|
|
116
|
+
const vec = new Float32Array(dims);
|
|
117
|
+
for (let d = 0; d < dims; d++) {
|
|
118
|
+
const value = entry[d];
|
|
119
|
+
if (!Number.isFinite(value)) {
|
|
120
|
+
throw new Error(`Embeddings[${i}][${d}] must be a finite number.`);
|
|
121
|
+
}
|
|
122
|
+
vec[d] = value;
|
|
123
|
+
}
|
|
124
|
+
return vec;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function printUsage() {
|
|
129
|
+
console.log("Usage: knolo <input.json> [output.knolo] [--embeddings embeddings.json --model-id model-name]");
|
|
130
|
+
}
|
|
131
|
+
|
|
56
132
|
const buildPack = await loadBuildPack();
|
|
133
|
+
const { positional, flags } = parseArgs(process.argv.slice(2));
|
|
134
|
+
|
|
135
|
+
if (flags.help) {
|
|
136
|
+
printUsage();
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
57
139
|
|
|
58
|
-
const inFile =
|
|
59
|
-
const outFile =
|
|
140
|
+
const inFile = positional[0];
|
|
141
|
+
const outFile = positional[1] || "knowledge.knolo";
|
|
60
142
|
|
|
61
143
|
if (!inFile) {
|
|
62
|
-
|
|
144
|
+
printUsage();
|
|
63
145
|
process.exit(1);
|
|
64
146
|
}
|
|
65
147
|
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
148
|
+
try {
|
|
149
|
+
const rawText = readFileSync(inFile, "utf8");
|
|
150
|
+
const parsed = JSON.parse(rawText);
|
|
151
|
+
const docs = validateCliDocs(parsed);
|
|
152
|
+
|
|
153
|
+
let options;
|
|
154
|
+
if (flags.embeddingsPath || flags.modelId) {
|
|
155
|
+
if (!flags.embeddingsPath || !flags.modelId) {
|
|
156
|
+
throw new Error("Both --embeddings and --model-id are required when enabling semantic build output.");
|
|
157
|
+
}
|
|
158
|
+
const embeddings = loadEmbeddingsFromJson(flags.embeddingsPath, docs.length);
|
|
159
|
+
options = {
|
|
160
|
+
semantic: {
|
|
161
|
+
enabled: true,
|
|
162
|
+
modelId: flags.modelId,
|
|
163
|
+
embeddings,
|
|
164
|
+
quantization: { type: "int8_l2norm", perVectorScale: true },
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const bytes = await buildPack(docs, options);
|
|
170
|
+
writeFileSync(outFile, Buffer.from(bytes));
|
|
171
|
+
console.log(`wrote ${outFile}`);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
174
|
+
console.error(`knolo: ${message}`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
package/dist/builder.d.ts
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
export type BuildInputDoc = {
|
|
2
2
|
id?: string;
|
|
3
3
|
heading?: string;
|
|
4
|
+
namespace?: string;
|
|
4
5
|
text: string;
|
|
5
6
|
};
|
|
6
|
-
export
|
|
7
|
+
export type BuildPackOptions = {
|
|
8
|
+
semantic?: {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
modelId: string;
|
|
11
|
+
embeddings: Float32Array[];
|
|
12
|
+
quantization?: {
|
|
13
|
+
type: 'int8_l2norm';
|
|
14
|
+
perVectorScale?: true;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export declare function buildPack(docs: BuildInputDoc[], opts?: BuildPackOptions): Promise<Uint8Array>;
|
package/dist/builder.js
CHANGED
|
@@ -1,48 +1,61 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* builder.ts
|
|
3
3
|
*
|
|
4
|
-
* Build `.knolo` packs from input docs.
|
|
5
|
-
* and stores avgBlockLen in meta for
|
|
4
|
+
* Build `.knolo` packs from input docs. Persists headings/docIds/token lengths
|
|
5
|
+
* and stores avgBlockLen in meta for stable query-time normalization.
|
|
6
6
|
*/
|
|
7
7
|
import { buildIndex } from './indexer.js';
|
|
8
8
|
import { tokenize } from './tokenize.js';
|
|
9
9
|
import { getTextEncoder } from './utils/utf8.js';
|
|
10
|
-
|
|
10
|
+
import { encodeScaleF16, quantizeEmbeddingInt8L2Norm } from './semantic.js';
|
|
11
|
+
export async function buildPack(docs, opts = {}) {
|
|
12
|
+
const normalizedDocs = validateDocs(docs);
|
|
11
13
|
// Prepare blocks (strip MD) and carry heading/docId for optional boosts.
|
|
12
|
-
const blocks =
|
|
14
|
+
const blocks = normalizedDocs.map((d, i) => ({
|
|
13
15
|
id: i,
|
|
14
16
|
text: stripMd(d.text),
|
|
15
17
|
heading: d.heading,
|
|
16
18
|
}));
|
|
17
19
|
// Build index
|
|
18
20
|
const { lexicon, postings } = buildIndex(blocks);
|
|
19
|
-
|
|
20
|
-
const totalTokens =
|
|
21
|
+
const blockTokenLens = blocks.map((b) => tokenize(b.text).length);
|
|
22
|
+
const totalTokens = blockTokenLens.reduce((sum, len) => sum + len, 0);
|
|
21
23
|
const avgBlockLen = blocks.length ? totalTokens / blocks.length : 1;
|
|
22
24
|
const meta = {
|
|
23
|
-
version:
|
|
25
|
+
version: 3,
|
|
24
26
|
stats: {
|
|
25
|
-
docs:
|
|
27
|
+
docs: normalizedDocs.length,
|
|
26
28
|
blocks: blocks.length,
|
|
27
29
|
terms: lexicon.length,
|
|
28
30
|
avgBlockLen,
|
|
29
31
|
},
|
|
30
32
|
};
|
|
31
|
-
// Persist blocks as objects to optionally carry heading/docId
|
|
33
|
+
// Persist blocks as objects to optionally carry heading/docId/token length.
|
|
32
34
|
const blocksPayload = blocks.map((b, i) => ({
|
|
33
35
|
text: b.text,
|
|
34
36
|
heading: b.heading ?? null,
|
|
35
|
-
docId:
|
|
37
|
+
docId: normalizedDocs[i]?.id ?? null,
|
|
38
|
+
namespace: normalizedDocs[i]?.namespace ?? null,
|
|
39
|
+
len: blockTokenLens[i] ?? 0,
|
|
36
40
|
}));
|
|
37
41
|
// Encode sections
|
|
38
42
|
const enc = getTextEncoder();
|
|
39
43
|
const metaBytes = enc.encode(JSON.stringify(meta));
|
|
40
44
|
const lexBytes = enc.encode(JSON.stringify(lexicon));
|
|
41
45
|
const blocksBytes = enc.encode(JSON.stringify(blocksPayload));
|
|
46
|
+
const semanticEnabled = Boolean(opts.semantic?.enabled);
|
|
47
|
+
const semanticSection = semanticEnabled && opts.semantic
|
|
48
|
+
? buildSemanticSection(blocks.length, opts.semantic)
|
|
49
|
+
: undefined;
|
|
50
|
+
const semBytes = semanticSection ? enc.encode(JSON.stringify(semanticSection.semJson)) : undefined;
|
|
51
|
+
const semBlob = semanticSection?.semBlob;
|
|
42
52
|
const totalLength = 4 + metaBytes.length +
|
|
43
53
|
4 + lexBytes.length +
|
|
44
54
|
4 + postings.length * 4 +
|
|
45
|
-
4 + blocksBytes.length
|
|
55
|
+
4 + blocksBytes.length +
|
|
56
|
+
(semanticEnabled && semBytes && semBlob
|
|
57
|
+
? 4 + semBytes.length + 4 + semBlob.length
|
|
58
|
+
: 0);
|
|
46
59
|
const out = new Uint8Array(totalLength);
|
|
47
60
|
const dv = new DataView(out.buffer);
|
|
48
61
|
let offset = 0;
|
|
@@ -67,8 +80,87 @@ export async function buildPack(docs) {
|
|
|
67
80
|
dv.setUint32(offset, blocksBytes.length, true);
|
|
68
81
|
offset += 4;
|
|
69
82
|
out.set(blocksBytes, offset);
|
|
83
|
+
offset += blocksBytes.length;
|
|
84
|
+
if (semanticEnabled && semBytes && semBlob) {
|
|
85
|
+
dv.setUint32(offset, semBytes.length, true);
|
|
86
|
+
offset += 4;
|
|
87
|
+
out.set(semBytes, offset);
|
|
88
|
+
offset += semBytes.length;
|
|
89
|
+
dv.setUint32(offset, semBlob.length, true);
|
|
90
|
+
offset += 4;
|
|
91
|
+
out.set(semBlob, offset);
|
|
92
|
+
}
|
|
70
93
|
return out;
|
|
71
94
|
}
|
|
95
|
+
function buildSemanticSection(blockCount, semantic) {
|
|
96
|
+
const { embeddings } = semantic;
|
|
97
|
+
if (!Array.isArray(embeddings) || embeddings.length !== blockCount) {
|
|
98
|
+
throw new Error(`semantic.embeddings must be provided with one embedding per block (expected ${blockCount}).`);
|
|
99
|
+
}
|
|
100
|
+
const quantizationType = semantic.quantization?.type ?? 'int8_l2norm';
|
|
101
|
+
if (quantizationType !== 'int8_l2norm') {
|
|
102
|
+
throw new Error(`Unsupported semantic quantization type: ${quantizationType}`);
|
|
103
|
+
}
|
|
104
|
+
const dims = embeddings[0]?.length ?? 0;
|
|
105
|
+
if (!dims)
|
|
106
|
+
throw new Error('semantic.embeddings must contain vectors with non-zero dimensions.');
|
|
107
|
+
const vecs = new Int8Array(embeddings.length * dims);
|
|
108
|
+
const scales = new Uint16Array(embeddings.length);
|
|
109
|
+
for (let i = 0; i < embeddings.length; i++) {
|
|
110
|
+
const embedding = embeddings[i];
|
|
111
|
+
if (!(embedding instanceof Float32Array)) {
|
|
112
|
+
throw new Error(`semantic.embeddings[${i}] must be a Float32Array.`);
|
|
113
|
+
}
|
|
114
|
+
if (embedding.length !== dims) {
|
|
115
|
+
throw new Error(`semantic.embeddings[${i}] dims mismatch: expected ${dims}, got ${embedding.length}.`);
|
|
116
|
+
}
|
|
117
|
+
const { q, scale } = quantizeEmbeddingInt8L2Norm(embedding);
|
|
118
|
+
vecs.set(q, i * dims);
|
|
119
|
+
scales[i] = encodeScaleF16(scale);
|
|
120
|
+
}
|
|
121
|
+
const vecByteOffset = 0;
|
|
122
|
+
const vecByteLength = vecs.byteLength;
|
|
123
|
+
const scalesByteOffset = vecByteLength;
|
|
124
|
+
const scalesByteLength = scales.byteLength;
|
|
125
|
+
const semBlob = new Uint8Array(vecByteLength + scalesByteLength);
|
|
126
|
+
semBlob.set(new Uint8Array(vecs.buffer, vecs.byteOffset, vecByteLength), vecByteOffset);
|
|
127
|
+
semBlob.set(new Uint8Array(scales.buffer, scales.byteOffset, scalesByteLength), scalesByteOffset);
|
|
128
|
+
const semJson = {
|
|
129
|
+
version: 1,
|
|
130
|
+
modelId: semantic.modelId,
|
|
131
|
+
dims,
|
|
132
|
+
encoding: 'int8_l2norm',
|
|
133
|
+
perVectorScale: true,
|
|
134
|
+
blocks: {
|
|
135
|
+
vectors: { byteOffset: vecByteOffset, length: vecs.length },
|
|
136
|
+
scales: { byteOffset: scalesByteOffset, length: scales.length, encoding: 'float16' },
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
return { semJson, semBlob };
|
|
140
|
+
}
|
|
141
|
+
function validateDocs(docs) {
|
|
142
|
+
if (!Array.isArray(docs)) {
|
|
143
|
+
throw new Error('buildPack expects an array of docs: [{ text, id?, heading?, namespace? }, ...]');
|
|
144
|
+
}
|
|
145
|
+
return docs.map((doc, i) => {
|
|
146
|
+
if (!doc || typeof doc !== 'object') {
|
|
147
|
+
throw new Error(`Invalid doc at index ${i}: expected an object with a string "text" field.`);
|
|
148
|
+
}
|
|
149
|
+
if (typeof doc.text !== 'string' || !doc.text.trim()) {
|
|
150
|
+
throw new Error(`Invalid doc at index ${i}: "text" must be a non-empty string.`);
|
|
151
|
+
}
|
|
152
|
+
if (doc.id !== undefined && typeof doc.id !== 'string') {
|
|
153
|
+
throw new Error(`Invalid doc at index ${i}: "id" must be a string when provided.`);
|
|
154
|
+
}
|
|
155
|
+
if (doc.heading !== undefined && typeof doc.heading !== 'string') {
|
|
156
|
+
throw new Error(`Invalid doc at index ${i}: "heading" must be a string when provided.`);
|
|
157
|
+
}
|
|
158
|
+
if (doc.namespace !== undefined && typeof doc.namespace !== 'string') {
|
|
159
|
+
throw new Error(`Invalid doc at index ${i}: "namespace" must be a string when provided.`);
|
|
160
|
+
}
|
|
161
|
+
return doc;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
72
164
|
/** Strip Markdown syntax with lightweight regexes (no deps). */
|
|
73
165
|
function stripMd(md) {
|
|
74
166
|
let text = md.replace(/```[\s\S]*?```/g, ' ');
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
export { mountPack } from './pack.js';
|
|
2
|
-
export { query } from './query.js';
|
|
1
|
+
export { mountPack, hasSemantic } from './pack.js';
|
|
2
|
+
export { query, lexConfidence, validateSemanticQueryOptions } from './query.js';
|
|
3
3
|
export { makeContextPatch } from './patch.js';
|
|
4
4
|
export { buildPack } from './builder.js';
|
|
5
|
+
export { quantizeEmbeddingInt8L2Norm, encodeScaleF16, decodeScaleF16 } from './semantic.js';
|
|
5
6
|
export type { MountOptions, PackMeta, Pack } from './pack.js';
|
|
6
7
|
export type { QueryOptions, Hit } from './query.js';
|
|
7
8
|
export type { ContextPatch } from './patch.js';
|
|
9
|
+
export type { BuildInputDoc, BuildPackOptions } from './builder.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
export { mountPack } from './pack.js';
|
|
3
|
-
export { query } from './query.js';
|
|
2
|
+
export { mountPack, hasSemantic } from './pack.js';
|
|
3
|
+
export { query, lexConfidence, validateSemanticQueryOptions } from './query.js';
|
|
4
4
|
export { makeContextPatch } from './patch.js';
|
|
5
5
|
export { buildPack } from './builder.js';
|
|
6
|
+
export { quantizeEmbeddingInt8L2Norm, encodeScaleF16, decodeScaleF16 } from './semantic.js';
|
package/dist/indexer.d.ts
CHANGED
|
@@ -13,8 +13,9 @@ export type IndexBuildResult = {
|
|
|
13
13
|
* sequences of blockId and positions for that term, with zeros as delimiters.
|
|
14
14
|
* The structure looks like:
|
|
15
15
|
*
|
|
16
|
-
* [termId, blockId, pos, pos, 0, blockId, pos, 0, 0, termId, ...]
|
|
16
|
+
* [termId, blockId+1, pos, pos, 0, blockId+1, pos, 0, 0, termId, ...]
|
|
17
17
|
*
|
|
18
|
+
* Block IDs are stored as bid+1 so that 0 can remain a sentinel delimiter.
|
|
18
19
|
* Each block section ends with a 0, and each term section ends with a 0. The
|
|
19
20
|
* entire array can be streamed sequentially without needing to know the sizes
|
|
20
21
|
* of individual lists ahead of time.
|
package/dist/indexer.js
CHANGED
|
@@ -14,8 +14,9 @@ import { tokenize } from "./tokenize.js";
|
|
|
14
14
|
* sequences of blockId and positions for that term, with zeros as delimiters.
|
|
15
15
|
* The structure looks like:
|
|
16
16
|
*
|
|
17
|
-
* [termId, blockId, pos, pos, 0, blockId, pos, 0, 0, termId, ...]
|
|
17
|
+
* [termId, blockId+1, pos, pos, 0, blockId+1, pos, 0, 0, termId, ...]
|
|
18
18
|
*
|
|
19
|
+
* Block IDs are stored as bid+1 so that 0 can remain a sentinel delimiter.
|
|
19
20
|
* Each block section ends with a 0, and each term section ends with a 0. The
|
|
20
21
|
* entire array can be streamed sequentially without needing to know the sizes
|
|
21
22
|
* of individual lists ahead of time.
|
|
@@ -60,7 +61,7 @@ export function buildIndex(blocks) {
|
|
|
60
61
|
for (const [tid, blockMap] of termBlockPositions) {
|
|
61
62
|
postings.push(tid);
|
|
62
63
|
for (const [bid, positions] of blockMap) {
|
|
63
|
-
postings.push(bid, ...positions, 0);
|
|
64
|
+
postings.push(bid + 1, ...positions, 0);
|
|
64
65
|
}
|
|
65
66
|
postings.push(0); // end of term
|
|
66
67
|
}
|
package/dist/pack.d.ts
CHANGED
|
@@ -17,5 +17,17 @@ export type Pack = {
|
|
|
17
17
|
blocks: string[];
|
|
18
18
|
headings?: (string | null)[];
|
|
19
19
|
docIds?: (string | null)[];
|
|
20
|
+
namespaces?: (string | null)[];
|
|
21
|
+
blockTokenLens?: number[];
|
|
22
|
+
semantic?: {
|
|
23
|
+
version: 1;
|
|
24
|
+
modelId: string;
|
|
25
|
+
dims: number;
|
|
26
|
+
encoding: 'int8_l2norm';
|
|
27
|
+
perVectorScale: boolean;
|
|
28
|
+
vecs: Int8Array;
|
|
29
|
+
scales?: Uint16Array;
|
|
30
|
+
};
|
|
20
31
|
};
|
|
32
|
+
export declare function hasSemantic(pack: Pack): boolean;
|
|
21
33
|
export declare function mountPack(opts: MountOptions): Promise<Pack>;
|
package/dist/pack.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* pack.ts
|
|
3
3
|
*
|
|
4
|
-
* Mount `.knolo` packs across Node, browsers, and RN/Expo.
|
|
5
|
-
* - blocks as string[] (v1) or object[] with { text, heading?, docId? }
|
|
4
|
+
* Mount `.knolo` packs across Node, browsers, and RN/Expo. Tolerant of:
|
|
5
|
+
* - blocks as string[] (v1) or object[] with { text, heading?, docId?, namespace?, len? }
|
|
6
6
|
* - meta.stats.avgBlockLen (optional)
|
|
7
7
|
* Includes RN/Expo-safe TextDecoder via ponyfill.
|
|
8
8
|
*/
|
|
9
9
|
import { getTextDecoder } from './utils/utf8.js';
|
|
10
|
+
export function hasSemantic(pack) {
|
|
11
|
+
return Boolean(pack.semantic && pack.semantic.dims > 0 && pack.semantic.vecs.length > 0);
|
|
12
|
+
}
|
|
10
13
|
export async function mountPack(opts) {
|
|
11
14
|
const buf = await resolveToBuffer(opts.src);
|
|
12
15
|
const dv = new DataView(buf);
|
|
@@ -33,14 +36,17 @@ export async function mountPack(opts) {
|
|
|
33
36
|
postings[i] = dv.getUint32(offset, true);
|
|
34
37
|
offset += 4;
|
|
35
38
|
}
|
|
36
|
-
// blocks (v1: string[]; v2: {text, heading?, docId?}[])
|
|
39
|
+
// blocks (v1: string[]; v2/v3: {text, heading?, docId?, namespace?, len?}[])
|
|
37
40
|
const blocksLen = dv.getUint32(offset, true);
|
|
38
41
|
offset += 4;
|
|
39
42
|
const blocksJson = dec.decode(new Uint8Array(buf, offset, blocksLen));
|
|
43
|
+
offset += blocksLen;
|
|
40
44
|
const parsed = JSON.parse(blocksJson);
|
|
41
45
|
let blocks = [];
|
|
42
46
|
let headings;
|
|
43
47
|
let docIds;
|
|
48
|
+
let namespaces;
|
|
49
|
+
let blockTokenLens;
|
|
44
50
|
if (Array.isArray(parsed) && parsed.length && typeof parsed[0] === 'string') {
|
|
45
51
|
// v1
|
|
46
52
|
blocks = parsed;
|
|
@@ -49,26 +55,71 @@ export async function mountPack(opts) {
|
|
|
49
55
|
blocks = [];
|
|
50
56
|
headings = [];
|
|
51
57
|
docIds = [];
|
|
58
|
+
namespaces = [];
|
|
59
|
+
blockTokenLens = [];
|
|
52
60
|
for (const it of parsed) {
|
|
53
61
|
if (it && typeof it === 'object') {
|
|
54
62
|
blocks.push(String(it.text ?? ''));
|
|
55
63
|
headings.push(it.heading ?? null);
|
|
56
64
|
docIds.push(it.docId ?? null);
|
|
65
|
+
namespaces.push(it.namespace ?? null);
|
|
66
|
+
blockTokenLens.push(typeof it.len === 'number' ? it.len : 0);
|
|
57
67
|
}
|
|
58
68
|
else {
|
|
59
69
|
blocks.push(String(it ?? ''));
|
|
60
70
|
headings.push(null);
|
|
61
71
|
docIds.push(null);
|
|
72
|
+
namespaces.push(null);
|
|
73
|
+
blockTokenLens.push(0);
|
|
62
74
|
}
|
|
63
75
|
}
|
|
64
76
|
}
|
|
65
77
|
else {
|
|
66
78
|
blocks = [];
|
|
67
79
|
}
|
|
68
|
-
|
|
80
|
+
let semantic;
|
|
81
|
+
if (offset < buf.byteLength) {
|
|
82
|
+
const semLen = dv.getUint32(offset, true);
|
|
83
|
+
offset += 4;
|
|
84
|
+
const semJson = dec.decode(new Uint8Array(buf, offset, semLen));
|
|
85
|
+
offset += semLen;
|
|
86
|
+
const sem = JSON.parse(semJson);
|
|
87
|
+
const semBlobLen = dv.getUint32(offset, true);
|
|
88
|
+
offset += 4;
|
|
89
|
+
const semBlob = new Uint8Array(buf, offset, semBlobLen);
|
|
90
|
+
semantic = parseSemanticSection(sem, semBlob);
|
|
91
|
+
}
|
|
92
|
+
return { meta, lexicon, postings, blocks, headings, docIds, namespaces, blockTokenLens, semantic };
|
|
93
|
+
}
|
|
94
|
+
function parseSemanticSection(sem, blob) {
|
|
95
|
+
const vectors = sem?.blocks?.vectors;
|
|
96
|
+
const scales = sem?.blocks?.scales;
|
|
97
|
+
const vecs = new Int8Array(blob.buffer, blob.byteOffset + Number(vectors?.byteOffset ?? 0), Number(vectors?.length ?? 0));
|
|
98
|
+
let scaleView;
|
|
99
|
+
if (scales) {
|
|
100
|
+
const scaleLen = Number(scales.length ?? 0);
|
|
101
|
+
const scaleOffset = Number(scales.byteOffset ?? 0);
|
|
102
|
+
const dv = new DataView(blob.buffer, blob.byteOffset + scaleOffset, scaleLen * 2);
|
|
103
|
+
scaleView = new Uint16Array(scaleLen);
|
|
104
|
+
for (let i = 0; i < scaleLen; i++) {
|
|
105
|
+
scaleView[i] = dv.getUint16(i * 2, true);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
version: 1,
|
|
110
|
+
modelId: String(sem?.modelId ?? ''),
|
|
111
|
+
dims: Number(sem?.dims ?? 0),
|
|
112
|
+
encoding: 'int8_l2norm',
|
|
113
|
+
perVectorScale: Boolean(sem?.perVectorScale),
|
|
114
|
+
vecs,
|
|
115
|
+
scales: scaleView,
|
|
116
|
+
};
|
|
69
117
|
}
|
|
70
118
|
async function resolveToBuffer(src) {
|
|
71
119
|
if (typeof src === 'string') {
|
|
120
|
+
if (isNodeRuntime() && isLikelyLocalPath(src)) {
|
|
121
|
+
return await readLocalFileAsBuffer(src);
|
|
122
|
+
}
|
|
72
123
|
const res = await fetch(src);
|
|
73
124
|
return await res.arrayBuffer();
|
|
74
125
|
}
|
|
@@ -81,3 +132,25 @@ async function resolveToBuffer(src) {
|
|
|
81
132
|
}
|
|
82
133
|
return src;
|
|
83
134
|
}
|
|
135
|
+
function isNodeRuntime() {
|
|
136
|
+
return typeof process !== 'undefined' && !!process.versions?.node;
|
|
137
|
+
}
|
|
138
|
+
function isLikelyLocalPath(value) {
|
|
139
|
+
if (value.startsWith('file://'))
|
|
140
|
+
return true;
|
|
141
|
+
if (value.startsWith('./') || value.startsWith('../') || value.startsWith('/') || value.startsWith('~'))
|
|
142
|
+
return true;
|
|
143
|
+
if (/^[A-Za-z]:[\\/]/.test(value))
|
|
144
|
+
return true; // Windows absolute path
|
|
145
|
+
if (/^[A-Za-z][A-Za-z\d+.-]*:/.test(value))
|
|
146
|
+
return false; // URL scheme
|
|
147
|
+
return true; // plain relative path like "knowledge.knolo"
|
|
148
|
+
}
|
|
149
|
+
async function readLocalFileAsBuffer(pathOrFileUrl) {
|
|
150
|
+
const { readFile } = await import('node:fs/promises');
|
|
151
|
+
const filePath = pathOrFileUrl.startsWith('file://')
|
|
152
|
+
? decodeURIComponent(new URL(pathOrFileUrl).pathname)
|
|
153
|
+
: pathOrFileUrl;
|
|
154
|
+
const data = await readFile(filePath);
|
|
155
|
+
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
156
|
+
}
|
package/dist/patch.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Hit } from
|
|
1
|
+
import type { Hit } from './query.js';
|
|
2
2
|
export type ContextPatch = {
|
|
3
3
|
background: string[];
|
|
4
4
|
snippets: Array<{
|
|
@@ -17,13 +17,6 @@ export type ContextPatch = {
|
|
|
17
17
|
evidence?: number[];
|
|
18
18
|
}>;
|
|
19
19
|
};
|
|
20
|
-
/** Assemble a context patch from an array of hits. The `budget` determines
|
|
21
|
-
* how many snippets and how much text to include in each snippet. Currently
|
|
22
|
-
* three budgets are supported:
|
|
23
|
-
* - `mini`: ~512 token contexts
|
|
24
|
-
* - `small`: ~1k token contexts
|
|
25
|
-
* - `full`: ~2k token contexts
|
|
26
|
-
*/
|
|
27
20
|
export declare function makeContextPatch(hits: Hit[], opts?: {
|
|
28
21
|
budget?: 'mini' | 'small' | 'full';
|
|
29
22
|
}): ContextPatch;
|
package/dist/patch.js
CHANGED
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* patch.ts
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* structured context patches for consumption by language models. A context
|
|
6
|
-
* patch includes a background summary and selected snippets. Future versions
|
|
7
|
-
* may include definitions, facts and other structured artifacts.
|
|
8
|
-
*/
|
|
9
|
-
/** Assemble a context patch from an array of hits. The `budget` determines
|
|
10
|
-
* how many snippets and how much text to include in each snippet. Currently
|
|
11
|
-
* three budgets are supported:
|
|
12
|
-
* - `mini`: ~512 token contexts
|
|
13
|
-
* - `small`: ~1k token contexts
|
|
14
|
-
* - `full`: ~2k token contexts
|
|
4
|
+
* Produces a compact, deterministic “context patch” from ranked hits.
|
|
15
5
|
*/
|
|
16
6
|
export function makeContextPatch(hits, opts = {}) {
|
|
17
7
|
const budget = opts.budget ?? 'small';
|
|
@@ -23,6 +13,7 @@ export function makeContextPatch(hits, opts = {}) {
|
|
|
23
13
|
const limit = limits[budget];
|
|
24
14
|
const snippets = hits.slice(0, limit.snippets).map((h) => ({
|
|
25
15
|
text: truncate(h.text, limit.chars),
|
|
16
|
+
source: h.source,
|
|
26
17
|
}));
|
|
27
18
|
// Build background summary from first two snippets by extracting first sentence
|
|
28
19
|
const background = snippets.slice(0, 2).map((s) => firstSentence(s.text));
|
|
@@ -33,18 +24,12 @@ export function makeContextPatch(hits, opts = {}) {
|
|
|
33
24
|
facts: [],
|
|
34
25
|
};
|
|
35
26
|
}
|
|
36
|
-
/** Extract the first sentence from a block of text. If no terminal punctuation
|
|
37
|
-
* is found, returns the first N characters up to a reasonable length.
|
|
38
|
-
*/
|
|
39
27
|
function firstSentence(text) {
|
|
40
28
|
const m = text.match(/^(.{10,200}?[.!?])\s/);
|
|
41
29
|
if (m)
|
|
42
30
|
return m[1];
|
|
43
31
|
return text.slice(0, 160);
|
|
44
32
|
}
|
|
45
|
-
/** Truncate text to a maximum length and append an ellipsis if it was
|
|
46
|
-
* truncated.
|
|
47
|
-
*/
|
|
48
33
|
function truncate(text, maxChars) {
|
|
49
34
|
return text.length > maxChars ? text.slice(0, maxChars) + '…' : text;
|
|
50
35
|
}
|
package/dist/query.d.ts
CHANGED
|
@@ -1,12 +1,40 @@
|
|
|
1
1
|
import type { Pack } from "./pack.js";
|
|
2
2
|
export type QueryOptions = {
|
|
3
3
|
topK?: number;
|
|
4
|
+
minScore?: number;
|
|
4
5
|
requirePhrases?: string[];
|
|
6
|
+
namespace?: string | string[];
|
|
7
|
+
source?: string | string[];
|
|
8
|
+
queryExpansion?: {
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
docs?: number;
|
|
11
|
+
terms?: number;
|
|
12
|
+
weight?: number;
|
|
13
|
+
minTermLength?: number;
|
|
14
|
+
};
|
|
15
|
+
semantic?: {
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
mode?: "rerank";
|
|
18
|
+
topN?: number;
|
|
19
|
+
minLexConfidence?: number;
|
|
20
|
+
blend?: {
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
wLex?: number;
|
|
23
|
+
wSem?: number;
|
|
24
|
+
};
|
|
25
|
+
queryEmbedding?: Float32Array;
|
|
26
|
+
force?: boolean;
|
|
27
|
+
};
|
|
5
28
|
};
|
|
29
|
+
export declare function validateSemanticQueryOptions(options?: QueryOptions["semantic"]): void;
|
|
6
30
|
export type Hit = {
|
|
7
31
|
blockId: number;
|
|
8
32
|
score: number;
|
|
9
33
|
text: string;
|
|
10
34
|
source?: string;
|
|
35
|
+
namespace?: string;
|
|
11
36
|
};
|
|
12
37
|
export declare function query(pack: Pack, q: string, opts?: QueryOptions): Hit[];
|
|
38
|
+
export declare function lexConfidence(hits: Array<{
|
|
39
|
+
score: number;
|
|
40
|
+
}>): number;
|