knolo-core 0.2.3 → 3.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/DOCS.md +242 -14
- package/README.md +342 -150
- package/bin/knolo.mjs +354 -36
- package/dist/agent.d.ts +53 -0
- package/dist/agent.js +175 -0
- package/dist/builder.d.ts +15 -1
- package/dist/builder.js +128 -14
- package/dist/index.d.ts +6 -2
- package/dist/index.js +4 -2
- package/dist/indexer.d.ts +2 -1
- package/dist/indexer.js +3 -2
- package/dist/pack.d.ts +14 -0
- package/dist/pack.js +96 -4
- package/dist/patch.d.ts +1 -8
- package/dist/patch.js +2 -17
- package/dist/query.d.ts +29 -0
- package/dist/query.js +324 -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 +5 -2
package/dist/query.js
CHANGED
|
@@ -13,18 +13,110 @@ import { rankBM25L } from "./rank.js";
|
|
|
13
13
|
import { minCoverSpan, proximityMultiplier } from "./quality/proximity.js";
|
|
14
14
|
import { diversifyAndDedupe } from "./quality/diversify.js";
|
|
15
15
|
import { knsSignature, knsDistance } from "./quality/signature.js";
|
|
16
|
+
import { decodeScaleF16, quantizeEmbeddingInt8L2Norm } from "./semantic.js";
|
|
17
|
+
export function validateQueryOptions(opts) {
|
|
18
|
+
if (!opts)
|
|
19
|
+
return;
|
|
20
|
+
if (opts.topK !== undefined && (!Number.isInteger(opts.topK) || opts.topK < 1)) {
|
|
21
|
+
throw new Error("query(...): topK must be a positive integer.");
|
|
22
|
+
}
|
|
23
|
+
if (opts.minScore !== undefined && (!Number.isFinite(opts.minScore) || opts.minScore < 0)) {
|
|
24
|
+
throw new Error("query(...): minScore must be a finite number >= 0.");
|
|
25
|
+
}
|
|
26
|
+
if (opts.requirePhrases !== undefined && (!Array.isArray(opts.requirePhrases) || opts.requirePhrases.some((p) => typeof p !== "string"))) {
|
|
27
|
+
throw new Error("query(...): requirePhrases must be an array of strings when provided.");
|
|
28
|
+
}
|
|
29
|
+
validateStringOrStringArrayOption("namespace", opts.namespace);
|
|
30
|
+
validateStringOrStringArrayOption("source", opts.source);
|
|
31
|
+
if (opts.queryExpansion) {
|
|
32
|
+
const qe = opts.queryExpansion;
|
|
33
|
+
if (qe.enabled !== undefined && typeof qe.enabled !== "boolean") {
|
|
34
|
+
throw new Error("query(...): queryExpansion.enabled must be a boolean when provided.");
|
|
35
|
+
}
|
|
36
|
+
if (qe.docs !== undefined && (!Number.isInteger(qe.docs) || qe.docs < 1)) {
|
|
37
|
+
throw new Error("query(...): queryExpansion.docs must be a positive integer.");
|
|
38
|
+
}
|
|
39
|
+
if (qe.terms !== undefined && (!Number.isInteger(qe.terms) || qe.terms < 1)) {
|
|
40
|
+
throw new Error("query(...): queryExpansion.terms must be a positive integer.");
|
|
41
|
+
}
|
|
42
|
+
if (qe.weight !== undefined && (!Number.isFinite(qe.weight) || qe.weight < 0)) {
|
|
43
|
+
throw new Error("query(...): queryExpansion.weight must be a finite number >= 0.");
|
|
44
|
+
}
|
|
45
|
+
if (qe.minTermLength !== undefined && (!Number.isInteger(qe.minTermLength) || qe.minTermLength < 1)) {
|
|
46
|
+
throw new Error("query(...): queryExpansion.minTermLength must be a positive integer.");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
validateSemanticQueryOptions(opts.semantic);
|
|
50
|
+
}
|
|
51
|
+
export function validateSemanticQueryOptions(options) {
|
|
52
|
+
if (!options)
|
|
53
|
+
return;
|
|
54
|
+
if (options.enabled !== undefined && typeof options.enabled !== "boolean") {
|
|
55
|
+
throw new Error("query(...): semantic.enabled must be a boolean when provided.");
|
|
56
|
+
}
|
|
57
|
+
if (options.mode !== undefined && options.mode !== "rerank") {
|
|
58
|
+
throw new Error('query(...): semantic.mode currently only supports "rerank".');
|
|
59
|
+
}
|
|
60
|
+
if (options.topN !== undefined && (!Number.isInteger(options.topN) || options.topN < 1)) {
|
|
61
|
+
throw new Error("query(...): semantic.topN must be a positive integer.");
|
|
62
|
+
}
|
|
63
|
+
if (options.minLexConfidence !== undefined && (!Number.isFinite(options.minLexConfidence) || options.minLexConfidence < 0 || options.minLexConfidence > 1)) {
|
|
64
|
+
throw new Error("query(...): semantic.minLexConfidence must be a finite number between 0 and 1.");
|
|
65
|
+
}
|
|
66
|
+
if (options.queryEmbedding !== undefined && !(options.queryEmbedding instanceof Float32Array)) {
|
|
67
|
+
throw new Error("query(...): semantic.queryEmbedding must be a Float32Array.");
|
|
68
|
+
}
|
|
69
|
+
if (options.blend) {
|
|
70
|
+
if (options.blend.enabled !== undefined && typeof options.blend.enabled !== "boolean") {
|
|
71
|
+
throw new Error("query(...): semantic.blend.enabled must be a boolean when provided.");
|
|
72
|
+
}
|
|
73
|
+
if (options.blend.wLex !== undefined && (!Number.isFinite(options.blend.wLex) || options.blend.wLex < 0)) {
|
|
74
|
+
throw new Error("query(...): semantic.blend.wLex must be a finite number >= 0.");
|
|
75
|
+
}
|
|
76
|
+
if (options.blend.wSem !== undefined && (!Number.isFinite(options.blend.wSem) || options.blend.wSem < 0)) {
|
|
77
|
+
throw new Error("query(...): semantic.blend.wSem must be a finite number >= 0.");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (options.force !== undefined && typeof options.force !== "boolean") {
|
|
81
|
+
throw new Error("query(...): semantic.force must be a boolean when provided.");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
16
84
|
export function query(pack, q, opts = {}) {
|
|
85
|
+
validateQueryOptions(opts);
|
|
17
86
|
const topK = opts.topK ?? 10;
|
|
87
|
+
const minScore = Number.isFinite(opts.minScore) ? Math.max(0, opts.minScore) : 0;
|
|
88
|
+
const expansionOpts = {
|
|
89
|
+
enabled: opts.queryExpansion?.enabled ?? true,
|
|
90
|
+
docs: Math.max(1, opts.queryExpansion?.docs ?? 3),
|
|
91
|
+
terms: Math.max(1, opts.queryExpansion?.terms ?? 4),
|
|
92
|
+
weight: Math.max(0, opts.queryExpansion?.weight ?? 0.35),
|
|
93
|
+
minTermLength: Math.max(2, opts.queryExpansion?.minTermLength ?? 3),
|
|
94
|
+
};
|
|
95
|
+
const semanticOpts = {
|
|
96
|
+
enabled: opts.semantic?.enabled ?? false,
|
|
97
|
+
mode: opts.semantic?.mode ?? "rerank",
|
|
98
|
+
topN: Math.max(1, opts.semantic?.topN ?? 50),
|
|
99
|
+
minLexConfidence: clamp01(opts.semantic?.minLexConfidence ?? 0.35),
|
|
100
|
+
blend: {
|
|
101
|
+
enabled: opts.semantic?.blend?.enabled ?? true,
|
|
102
|
+
wLex: Math.max(0, opts.semantic?.blend?.wLex ?? 0.75),
|
|
103
|
+
wSem: Math.max(0, opts.semantic?.blend?.wSem ?? 0.25),
|
|
104
|
+
},
|
|
105
|
+
queryEmbedding: opts.semantic?.queryEmbedding,
|
|
106
|
+
force: opts.semantic?.force ?? false,
|
|
107
|
+
};
|
|
18
108
|
// --- Query parsing
|
|
19
109
|
const normTokens = tokenize(q).map((t) => t.term);
|
|
20
110
|
// Normalize quoted phrases from q
|
|
21
|
-
const quotedRaw = parsePhrases(q);
|
|
22
|
-
const quoted = quotedRaw.map(seq => seq.map(t => normalize(t)).flatMap(s => s.split(/\s+/)).filter(Boolean));
|
|
111
|
+
const quotedRaw = parsePhrases(q);
|
|
112
|
+
const quoted = quotedRaw.map((seq) => seq.map((t) => normalize(t)).flatMap((s) => s.split(/\s+/)).filter(Boolean));
|
|
23
113
|
// Normalize requirePhrases the same way
|
|
24
114
|
const extraReq = (opts.requirePhrases ?? [])
|
|
25
|
-
.map(s => tokenize(s).map(t => t.term))
|
|
26
|
-
.filter(arr => arr.length > 0);
|
|
115
|
+
.map((s) => tokenize(s).map((t) => t.term))
|
|
116
|
+
.filter((arr) => arr.length > 0);
|
|
27
117
|
const requiredPhrases = [...quoted, ...extraReq];
|
|
118
|
+
const namespaceFilter = normalizeNamespaceFilter(opts.namespace);
|
|
119
|
+
const sourceFilter = normalizeSourceFilter(opts.source);
|
|
28
120
|
// --- Term ids for the free (unquoted) tokens in q
|
|
29
121
|
const termIds = normTokens
|
|
30
122
|
.map((t) => pack.lexicon.get(t))
|
|
@@ -33,39 +125,53 @@ export function query(pack, q, opts = {}) {
|
|
|
33
125
|
const termSet = new Set(termIds);
|
|
34
126
|
// --- Candidate map
|
|
35
127
|
const candidates = new Map();
|
|
128
|
+
// Query-time document frequency collection for BM25 IDF.
|
|
129
|
+
const dfs = new Map();
|
|
130
|
+
const usesOffsetBlockIds = (pack.meta?.version ?? 1) >= 3;
|
|
36
131
|
// Helper to harvest postings for a given set of termIds into candidates
|
|
37
|
-
function scanForTermIds(
|
|
132
|
+
function scanForTermIds(idWeights, cfg = { collectPositions: true, createCandidates: true }) {
|
|
38
133
|
const p = pack.postings;
|
|
39
134
|
let i = 0;
|
|
40
135
|
while (i < p.length) {
|
|
41
136
|
const tid = p[i++];
|
|
42
137
|
if (tid === 0)
|
|
43
138
|
continue;
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
139
|
+
const weight = idWeights.get(tid) ?? 0;
|
|
140
|
+
const relevant = weight > 0;
|
|
141
|
+
let termDf = 0;
|
|
142
|
+
let encodedBid = p[i++];
|
|
143
|
+
while (encodedBid !== 0) {
|
|
144
|
+
const bid = usesOffsetBlockIds ? encodedBid - 1 : encodedBid;
|
|
47
145
|
let pos = p[i++];
|
|
48
146
|
const positions = [];
|
|
49
147
|
while (pos !== 0) {
|
|
50
148
|
positions.push(pos);
|
|
51
149
|
pos = p[i++];
|
|
52
150
|
}
|
|
53
|
-
|
|
151
|
+
termDf++;
|
|
152
|
+
if (relevant && bid >= 0) {
|
|
54
153
|
let entry = candidates.get(bid);
|
|
55
|
-
if (!entry) {
|
|
154
|
+
if (!entry && cfg.createCandidates !== false) {
|
|
56
155
|
entry = { tf: new Map(), pos: new Map() };
|
|
57
156
|
candidates.set(bid, entry);
|
|
58
157
|
}
|
|
59
|
-
entry
|
|
60
|
-
|
|
158
|
+
if (entry) {
|
|
159
|
+
const prevTf = entry.tf.get(tid) ?? 0;
|
|
160
|
+
entry.tf.set(tid, prevTf + positions.length * weight);
|
|
161
|
+
if (cfg.collectPositions !== false) {
|
|
162
|
+
entry.pos.set(tid, positions);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
61
165
|
}
|
|
62
|
-
|
|
166
|
+
encodedBid = p[i++];
|
|
63
167
|
}
|
|
168
|
+
if (relevant)
|
|
169
|
+
dfs.set(tid, termDf);
|
|
64
170
|
}
|
|
65
171
|
}
|
|
66
172
|
// 1) Scan using tokens from q (if any)
|
|
67
173
|
if (termSet.size > 0) {
|
|
68
|
-
scanForTermIds(termSet);
|
|
174
|
+
scanForTermIds(new Map(Array.from(termSet.values(), (tid) => [tid, 1])));
|
|
69
175
|
}
|
|
70
176
|
// 2) Phrase-first rescue:
|
|
71
177
|
// If nothing matched the free tokens, but we do have required phrases,
|
|
@@ -80,7 +186,27 @@ export function query(pack, q, opts = {}) {
|
|
|
80
186
|
}
|
|
81
187
|
}
|
|
82
188
|
if (phraseTokenIds.size > 0) {
|
|
83
|
-
scanForTermIds(phraseTokenIds);
|
|
189
|
+
scanForTermIds(new Map(Array.from(phraseTokenIds.values(), (tid) => [tid, 1])));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// --- Namespace filtering
|
|
193
|
+
if (namespaceFilter.size > 0) {
|
|
194
|
+
for (const bid of [...candidates.keys()]) {
|
|
195
|
+
const ns = pack.namespaces?.[bid];
|
|
196
|
+
const normalizedNs = typeof ns === "string" ? normalize(ns) : "";
|
|
197
|
+
if (!normalizedNs || !namespaceFilter.has(normalizedNs)) {
|
|
198
|
+
candidates.delete(bid);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// --- Source/docId filtering
|
|
203
|
+
if (sourceFilter.size > 0) {
|
|
204
|
+
for (const bid of [...candidates.keys()]) {
|
|
205
|
+
const source = pack.docIds?.[bid];
|
|
206
|
+
const normalizedSource = typeof source === "string" ? normalize(source) : "";
|
|
207
|
+
if (!normalizedSource || !sourceFilter.has(normalizedSource)) {
|
|
208
|
+
candidates.delete(bid);
|
|
209
|
+
}
|
|
84
210
|
}
|
|
85
211
|
}
|
|
86
212
|
// --- Phrase enforcement (now that we have some candidates)
|
|
@@ -119,11 +245,30 @@ export function query(pack, q, opts = {}) {
|
|
|
119
245
|
(pack.blocks.length
|
|
120
246
|
? pack.blocks.reduce((s, b) => s + tokenize(b).length, 0) / pack.blocks.length
|
|
121
247
|
: 1);
|
|
122
|
-
const
|
|
248
|
+
const docCount = pack.meta?.stats?.blocks ?? pack.blocks.length;
|
|
249
|
+
let prelim = rankBM25L(candidates, avgLen, docCount, dfs, pack.blockTokenLens, {
|
|
123
250
|
proximityBonus: (cand) => proximityMultiplier(minCoverSpan(cand.pos)),
|
|
124
251
|
});
|
|
252
|
+
if (expansionOpts.enabled && prelim.length > 0) {
|
|
253
|
+
const expansionWeights = deriveExpansionTerms(pack, prelim, termSet, requiredPhrases, expansionOpts);
|
|
254
|
+
if (expansionWeights.size > 0) {
|
|
255
|
+
scanForTermIds(expansionWeights, { collectPositions: false, createCandidates: true });
|
|
256
|
+
prelim = rankBM25L(candidates, avgLen, docCount, dfs, pack.blockTokenLens, {
|
|
257
|
+
proximityBonus: (cand) => proximityMultiplier(minCoverSpan(cand.pos)),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
125
261
|
if (prelim.length === 0)
|
|
126
262
|
return [];
|
|
263
|
+
if (minScore > 0) {
|
|
264
|
+
prelim = prelim.filter((item) => item.score >= minScore);
|
|
265
|
+
if (prelim.length === 0)
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
const confidence = lexConfidence(prelim);
|
|
269
|
+
if (shouldRerankWithSemantic(pack, semanticOpts, confidence)) {
|
|
270
|
+
prelim = rerankLexicalHitsWithSemantic(pack, prelim, semanticOpts);
|
|
271
|
+
}
|
|
127
272
|
// --- KNS tie-breaker + de-dup/MMR
|
|
128
273
|
const qSig = knsSignature(normalize(q));
|
|
129
274
|
const pool = prelim.slice(0, topK * 5).map((r) => {
|
|
@@ -134,17 +279,158 @@ export function query(pack, q, opts = {}) {
|
|
|
134
279
|
score: r.score * boost,
|
|
135
280
|
text,
|
|
136
281
|
source: pack.docIds?.[r.blockId] ?? undefined,
|
|
282
|
+
namespace: pack.namespaces?.[r.blockId] ?? undefined,
|
|
137
283
|
};
|
|
138
284
|
});
|
|
139
285
|
const finalHits = diversifyAndDedupe(pool, { k: topK });
|
|
140
286
|
return finalHits;
|
|
141
287
|
}
|
|
288
|
+
export function lexConfidence(hits) {
|
|
289
|
+
if (hits.length === 0)
|
|
290
|
+
return 0;
|
|
291
|
+
const top1 = Math.max(0, hits[0]?.score ?? 0);
|
|
292
|
+
const top2 = Math.max(0, hits[1]?.score ?? 0);
|
|
293
|
+
const gapRatio = top1 > 0 ? clamp01((top1 - top2) / top1) : 0;
|
|
294
|
+
const strength = top1 > 0 ? top1 / (top1 + 1) : 0;
|
|
295
|
+
return clamp01(0.65 * gapRatio + 0.35 * strength);
|
|
296
|
+
}
|
|
297
|
+
function shouldRerankWithSemantic(pack, opts, confidence) {
|
|
298
|
+
if (!opts.enabled || opts.mode !== "rerank")
|
|
299
|
+
return false;
|
|
300
|
+
if (!pack.semantic)
|
|
301
|
+
return false;
|
|
302
|
+
if (!opts.queryEmbedding) {
|
|
303
|
+
throw new Error("query(...): semantic.queryEmbedding (Float32Array) is required when semantic.enabled=true.");
|
|
304
|
+
}
|
|
305
|
+
return opts.force || confidence < opts.minLexConfidence;
|
|
306
|
+
}
|
|
307
|
+
function rerankLexicalHitsWithSemantic(pack, prelim, opts) {
|
|
308
|
+
const sem = pack.semantic;
|
|
309
|
+
if (!sem || !opts.queryEmbedding)
|
|
310
|
+
return prelim;
|
|
311
|
+
if (sem.dims <= 0 || sem.vecs.length === 0 || sem.dims !== opts.queryEmbedding.length)
|
|
312
|
+
return prelim;
|
|
313
|
+
const topN = Math.min(opts.topN, prelim.length);
|
|
314
|
+
const rerankSlice = prelim.slice(0, topN);
|
|
315
|
+
const tail = prelim.slice(topN);
|
|
316
|
+
const lexScores = new Float64Array(topN);
|
|
317
|
+
for (let i = 0; i < topN; i++)
|
|
318
|
+
lexScores[i] = rerankSlice[i].score;
|
|
319
|
+
const normLex = minMaxNormalizeTyped(lexScores);
|
|
320
|
+
const quantizedQuery = quantizeEmbeddingInt8L2Norm(opts.queryEmbedding);
|
|
321
|
+
const semScores = scoreSemanticInt8(quantizedQuery.q, quantizedQuery.scale, sem, rerankSlice);
|
|
322
|
+
const normSem = minMaxNormalizeTyped(semScores);
|
|
323
|
+
const denom = opts.blend.wLex + opts.blend.wSem;
|
|
324
|
+
const wLex = denom > 0 ? opts.blend.wLex / denom : 0.5;
|
|
325
|
+
const wSem = denom > 0 ? opts.blend.wSem / denom : 0.5;
|
|
326
|
+
const reranked = new Array(topN);
|
|
327
|
+
for (let i = 0; i < topN; i++) {
|
|
328
|
+
const hit = rerankSlice[i];
|
|
329
|
+
reranked[i] = {
|
|
330
|
+
blockId: hit.blockId,
|
|
331
|
+
score: opts.blend.enabled ? wLex * normLex[i] + wSem * normSem[i] : semScores[i],
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
reranked.sort((a, b) => b.score - a.score || a.blockId - b.blockId);
|
|
335
|
+
return [...reranked, ...tail];
|
|
336
|
+
}
|
|
337
|
+
function scoreSemanticInt8(queryQ, queryScale, semantic, hits) {
|
|
338
|
+
const scores = new Float64Array(hits.length);
|
|
339
|
+
const dims = semantic.dims;
|
|
340
|
+
if (queryQ.length !== dims || queryScale === 0)
|
|
341
|
+
return scores;
|
|
342
|
+
const vecs = semantic.vecs;
|
|
343
|
+
const scales = semantic.scales;
|
|
344
|
+
for (let h = 0; h < hits.length; h++) {
|
|
345
|
+
const blockId = hits[h].blockId;
|
|
346
|
+
if (blockId < 0)
|
|
347
|
+
continue;
|
|
348
|
+
const base = blockId * dims;
|
|
349
|
+
if (base + dims > vecs.length)
|
|
350
|
+
continue;
|
|
351
|
+
let dot = 0;
|
|
352
|
+
for (let i = 0; i < dims; i++) {
|
|
353
|
+
dot += queryQ[i] * vecs[base + i];
|
|
354
|
+
}
|
|
355
|
+
const blockScale = scales?.[blockId] !== undefined ? decodeScaleF16(scales[blockId]) : (1 / 127);
|
|
356
|
+
scores[h] = dot * queryScale * blockScale;
|
|
357
|
+
}
|
|
358
|
+
return scores;
|
|
359
|
+
}
|
|
360
|
+
function minMaxNormalizeTyped(values) {
|
|
361
|
+
if (values.length === 0)
|
|
362
|
+
return values;
|
|
363
|
+
let min = Infinity;
|
|
364
|
+
let max = -Infinity;
|
|
365
|
+
for (let i = 0; i < values.length; i++) {
|
|
366
|
+
const v = values[i];
|
|
367
|
+
if (v < min)
|
|
368
|
+
min = v;
|
|
369
|
+
if (v > max)
|
|
370
|
+
max = v;
|
|
371
|
+
}
|
|
372
|
+
if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) {
|
|
373
|
+
const out = new Float64Array(values.length);
|
|
374
|
+
out.fill(1);
|
|
375
|
+
return out;
|
|
376
|
+
}
|
|
377
|
+
const out = new Float64Array(values.length);
|
|
378
|
+
const span = max - min;
|
|
379
|
+
for (let i = 0; i < values.length; i++) {
|
|
380
|
+
out[i] = clamp01((values[i] - min) / span);
|
|
381
|
+
}
|
|
382
|
+
return out;
|
|
383
|
+
}
|
|
384
|
+
function clamp01(v) {
|
|
385
|
+
if (!Number.isFinite(v))
|
|
386
|
+
return 0;
|
|
387
|
+
if (v < 0)
|
|
388
|
+
return 0;
|
|
389
|
+
if (v > 1)
|
|
390
|
+
return 1;
|
|
391
|
+
return v;
|
|
392
|
+
}
|
|
393
|
+
function deriveExpansionTerms(pack, prelim, baseTermSet, requiredPhrases, opts) {
|
|
394
|
+
if (prelim.length === 0 || opts.weight <= 0)
|
|
395
|
+
return new Map();
|
|
396
|
+
const forbidden = new Set(baseTermSet);
|
|
397
|
+
for (const seq of requiredPhrases) {
|
|
398
|
+
for (const term of seq) {
|
|
399
|
+
const tid = pack.lexicon.get(term);
|
|
400
|
+
if (tid !== undefined)
|
|
401
|
+
forbidden.add(tid);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const cap = Math.min(opts.docs, prelim.length);
|
|
405
|
+
const bestScore = Math.max(prelim[0]?.score ?? 0, 1e-6);
|
|
406
|
+
const termScores = new Map();
|
|
407
|
+
for (let i = 0; i < cap; i++) {
|
|
408
|
+
const item = prelim[i];
|
|
409
|
+
const text = pack.blocks[item.blockId] ?? "";
|
|
410
|
+
const docWeight = Math.max(item.score / bestScore, 0.2);
|
|
411
|
+
const localTfs = new Map();
|
|
412
|
+
for (const tok of tokenize(text)) {
|
|
413
|
+
if (tok.term.length < opts.minTermLength)
|
|
414
|
+
continue;
|
|
415
|
+
const tid = pack.lexicon.get(tok.term);
|
|
416
|
+
if (tid === undefined || forbidden.has(tid))
|
|
417
|
+
continue;
|
|
418
|
+
localTfs.set(tid, (localTfs.get(tid) ?? 0) + 1);
|
|
419
|
+
}
|
|
420
|
+
for (const [tid, tf] of localTfs) {
|
|
421
|
+
termScores.set(tid, (termScores.get(tid) ?? 0) + tf * docWeight);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const selected = [...termScores.entries()]
|
|
425
|
+
.sort((a, b) => b[1] - a[1])
|
|
426
|
+
.slice(0, opts.terms);
|
|
427
|
+
return new Map(selected.map(([tid, score]) => [tid, opts.weight * Math.max(0.5, Math.min(1.5, score))]));
|
|
428
|
+
}
|
|
142
429
|
/** Ordered phrase check using the SAME tokenizer/normalizer path as the index. */
|
|
143
430
|
function containsPhrase(text, seq) {
|
|
144
431
|
if (seq.length === 0)
|
|
145
432
|
return false;
|
|
146
|
-
|
|
147
|
-
const seqNorm = tokenize(seq.join(" ")).map(t => t.term);
|
|
433
|
+
const seqNorm = tokenize(seq.join(" ")).map((t) => t.term);
|
|
148
434
|
const toks = tokenize(text).map((t) => t.term);
|
|
149
435
|
outer: for (let i = 0; i <= toks.length - seqNorm.length; i++) {
|
|
150
436
|
for (let j = 0; j < seqNorm.length; j++) {
|
|
@@ -155,3 +441,23 @@ function containsPhrase(text, seq) {
|
|
|
155
441
|
}
|
|
156
442
|
return false;
|
|
157
443
|
}
|
|
444
|
+
function normalizeNamespaceFilter(input) {
|
|
445
|
+
if (input === undefined)
|
|
446
|
+
return new Set();
|
|
447
|
+
const values = Array.isArray(input) ? input : [input];
|
|
448
|
+
return new Set(values.map((v) => normalize(v)).filter(Boolean));
|
|
449
|
+
}
|
|
450
|
+
function normalizeSourceFilter(input) {
|
|
451
|
+
if (input === undefined)
|
|
452
|
+
return new Set();
|
|
453
|
+
const values = Array.isArray(input) ? input : [input];
|
|
454
|
+
return new Set(values.map((v) => normalize(v)).filter(Boolean));
|
|
455
|
+
}
|
|
456
|
+
function validateStringOrStringArrayOption(name, value) {
|
|
457
|
+
if (value === undefined)
|
|
458
|
+
return;
|
|
459
|
+
const valid = typeof value === "string" || (Array.isArray(value) && value.every((entry) => typeof entry === "string"));
|
|
460
|
+
if (!valid) {
|
|
461
|
+
throw new Error(`query(...): ${name} must be a string or an array of strings when provided.`);
|
|
462
|
+
}
|
|
463
|
+
}
|
package/dist/rank.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export declare function rankBM25L(candidates: Map<number, {
|
|
|
15
15
|
pos?: Map<number, number[]>;
|
|
16
16
|
hasPhrase?: boolean;
|
|
17
17
|
headingScore?: number;
|
|
18
|
-
}>, avgLen: number, opts?: RankOptions): Array<{
|
|
18
|
+
}>, avgLen: number, docCount: number, dfs: Map<number, number>, blockTokenLens?: number[], opts?: RankOptions): Array<{
|
|
19
19
|
blockId: number;
|
|
20
20
|
score: number;
|
|
21
21
|
}>;
|
package/dist/rank.js
CHANGED
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
* rank.ts
|
|
3
3
|
* BM25L ranker with optional heading/phrase boosts and a proximity bonus hook.
|
|
4
4
|
*/
|
|
5
|
-
export function rankBM25L(candidates, avgLen, opts = {}) {
|
|
5
|
+
export function rankBM25L(candidates, avgLen, docCount, dfs, blockTokenLens, opts = {}) {
|
|
6
6
|
const k1 = opts.k1 ?? 1.5;
|
|
7
7
|
const b = opts.b ?? 0.75;
|
|
8
8
|
const headingBoost = opts.headingBoost ?? 0.3;
|
|
9
9
|
const phraseBoost = opts.phraseBoost ?? 0.6;
|
|
10
10
|
const results = [];
|
|
11
11
|
for (const [bid, data] of candidates) {
|
|
12
|
-
const len = Array.from(data.tf.values()).reduce((sum, tf) => sum + tf, 0) || 1;
|
|
12
|
+
const len = blockTokenLens?.[bid] ?? (Array.from(data.tf.values()).reduce((sum, tf) => sum + tf, 0) || 1);
|
|
13
13
|
let score = 0;
|
|
14
|
-
for (const [, tf] of data.tf) {
|
|
15
|
-
const
|
|
14
|
+
for (const [tid, tf] of data.tf) {
|
|
15
|
+
const df = dfs.get(tid) ?? 0;
|
|
16
|
+
const idf = Math.log(1 + (docCount - df + 0.5) / (df + 0.5));
|
|
16
17
|
const numer = tf * (k1 + 1);
|
|
17
18
|
const denom = tf + k1 * (1 - b + b * (len / avgLen));
|
|
18
19
|
score += idf * (numer / denom);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type QuantizedVector = {
|
|
2
|
+
q: Int8Array;
|
|
3
|
+
scale: number;
|
|
4
|
+
};
|
|
5
|
+
export declare function quantizeEmbeddingInt8L2Norm(embedding: Float32Array): QuantizedVector;
|
|
6
|
+
export declare function encodeScaleF16(scale: number): number;
|
|
7
|
+
export declare function decodeScaleF16(encoded: number): number;
|
package/dist/semantic.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export function quantizeEmbeddingInt8L2Norm(embedding) {
|
|
2
|
+
const dims = embedding.length;
|
|
3
|
+
const normalized = new Float32Array(dims);
|
|
4
|
+
let normSq = 0;
|
|
5
|
+
for (let i = 0; i < dims; i++)
|
|
6
|
+
normSq += embedding[i] * embedding[i];
|
|
7
|
+
const norm = Math.sqrt(normSq);
|
|
8
|
+
if (norm === 0) {
|
|
9
|
+
return { q: new Int8Array(dims), scale: 0 };
|
|
10
|
+
}
|
|
11
|
+
let maxAbs = 0;
|
|
12
|
+
for (let i = 0; i < dims; i++) {
|
|
13
|
+
const value = embedding[i] / norm;
|
|
14
|
+
normalized[i] = value;
|
|
15
|
+
const abs = Math.abs(value);
|
|
16
|
+
if (abs > maxAbs)
|
|
17
|
+
maxAbs = abs;
|
|
18
|
+
}
|
|
19
|
+
const scale = maxAbs / 127;
|
|
20
|
+
if (scale === 0) {
|
|
21
|
+
return { q: new Int8Array(dims), scale: 0 };
|
|
22
|
+
}
|
|
23
|
+
const q = new Int8Array(dims);
|
|
24
|
+
for (let i = 0; i < dims; i++) {
|
|
25
|
+
const quantized = Math.round(normalized[i] / scale);
|
|
26
|
+
q[i] = clampInt8(quantized);
|
|
27
|
+
}
|
|
28
|
+
return { q, scale };
|
|
29
|
+
}
|
|
30
|
+
export function encodeScaleF16(scale) {
|
|
31
|
+
return float32ToFloat16(scale);
|
|
32
|
+
}
|
|
33
|
+
export function decodeScaleF16(encoded) {
|
|
34
|
+
return float16ToFloat32(encoded);
|
|
35
|
+
}
|
|
36
|
+
function clampInt8(value) {
|
|
37
|
+
if (value > 127)
|
|
38
|
+
return 127;
|
|
39
|
+
if (value < -127)
|
|
40
|
+
return -127;
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
function float32ToFloat16(value) {
|
|
44
|
+
if (Number.isNaN(value))
|
|
45
|
+
return 0x7e00;
|
|
46
|
+
if (value === Infinity)
|
|
47
|
+
return 0x7c00;
|
|
48
|
+
if (value === -Infinity)
|
|
49
|
+
return 0xfc00;
|
|
50
|
+
const f32 = new Float32Array(1);
|
|
51
|
+
const u32 = new Uint32Array(f32.buffer);
|
|
52
|
+
f32[0] = value;
|
|
53
|
+
const bits = u32[0];
|
|
54
|
+
const sign = (bits >>> 16) & 0x8000;
|
|
55
|
+
let exp = (bits >>> 23) & 0xff;
|
|
56
|
+
let mantissa = bits & 0x7fffff;
|
|
57
|
+
if (exp === 0xff) {
|
|
58
|
+
return sign | (mantissa ? 0x7e00 : 0x7c00);
|
|
59
|
+
}
|
|
60
|
+
const halfExp = exp - 127 + 15;
|
|
61
|
+
if (halfExp >= 0x1f) {
|
|
62
|
+
return sign | 0x7c00;
|
|
63
|
+
}
|
|
64
|
+
if (halfExp <= 0) {
|
|
65
|
+
if (halfExp < -10)
|
|
66
|
+
return sign;
|
|
67
|
+
mantissa = (mantissa | 0x800000) >>> (1 - halfExp);
|
|
68
|
+
if (mantissa & 0x1000)
|
|
69
|
+
mantissa += 0x2000;
|
|
70
|
+
return sign | (mantissa >>> 13);
|
|
71
|
+
}
|
|
72
|
+
if (mantissa & 0x1000) {
|
|
73
|
+
mantissa += 0x2000;
|
|
74
|
+
if (mantissa & 0x800000) {
|
|
75
|
+
mantissa = 0;
|
|
76
|
+
exp += 1;
|
|
77
|
+
if (exp > 142)
|
|
78
|
+
return sign | 0x7c00;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return sign | (halfExp << 10) | (mantissa >>> 13);
|
|
82
|
+
}
|
|
83
|
+
function float16ToFloat32(bits) {
|
|
84
|
+
const sign = (bits & 0x8000) ? -1 : 1;
|
|
85
|
+
const exp = (bits >>> 10) & 0x1f;
|
|
86
|
+
const frac = bits & 0x03ff;
|
|
87
|
+
if (exp === 0) {
|
|
88
|
+
if (frac === 0)
|
|
89
|
+
return sign * 0;
|
|
90
|
+
return sign * Math.pow(2, -14) * (frac / 1024);
|
|
91
|
+
}
|
|
92
|
+
if (exp === 0x1f) {
|
|
93
|
+
if (frac === 0)
|
|
94
|
+
return sign * Infinity;
|
|
95
|
+
return NaN;
|
|
96
|
+
}
|
|
97
|
+
return sign * Math.pow(2, exp - 15) * (1 + frac / 1024);
|
|
98
|
+
}
|
package/dist/tokenize.js
CHANGED
|
@@ -42,7 +42,7 @@ export function tokenize(text) {
|
|
|
42
42
|
*/
|
|
43
43
|
export function parsePhrases(q) {
|
|
44
44
|
const parts = [];
|
|
45
|
-
const regex = /"([
|
|
45
|
+
const regex = /["“”]([^"“”]+)["“”]/g;
|
|
46
46
|
let match;
|
|
47
47
|
while ((match = regex.exec(q)) !== null) {
|
|
48
48
|
const phrase = match[1].trim().split(/\s+/);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "knolo-core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local-first knowledge packs for small LLMs.",
|
|
6
6
|
"keywords": [
|
|
@@ -33,7 +33,10 @@
|
|
|
33
33
|
"scripts": {
|
|
34
34
|
"build": "tsc -p tsconfig.json",
|
|
35
35
|
"prepublishOnly": "npm run build",
|
|
36
|
-
"smoke": "node scripts/smoke.mjs"
|
|
36
|
+
"smoke": "node scripts/smoke.mjs",
|
|
37
|
+
"test": "npm run build && node scripts/test.mjs",
|
|
38
|
+
"format": "prettier --write src/agent.ts src/pack.ts src/builder.ts src/index.ts scripts/test.mjs README.md",
|
|
39
|
+
"format:check": "prettier --check src/agent.ts src/pack.ts src/builder.ts src/index.ts scripts/test.mjs README.md"
|
|
37
40
|
},
|
|
38
41
|
"devDependencies": {
|
|
39
42
|
"typescript": "^5.5.0",
|