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/builder.js
CHANGED
|
@@ -1,48 +1,70 @@
|
|
|
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
|
+
import { validateAgentRegistry } from './agent.js';
|
|
12
|
+
export async function buildPack(docs, opts = {}) {
|
|
13
|
+
const normalizedDocs = validateDocs(docs);
|
|
11
14
|
// Prepare blocks (strip MD) and carry heading/docId for optional boosts.
|
|
12
|
-
const blocks =
|
|
15
|
+
const blocks = normalizedDocs.map((d, i) => ({
|
|
13
16
|
id: i,
|
|
14
17
|
text: stripMd(d.text),
|
|
15
18
|
heading: d.heading,
|
|
16
19
|
}));
|
|
17
20
|
// Build index
|
|
18
21
|
const { lexicon, postings } = buildIndex(blocks);
|
|
19
|
-
|
|
20
|
-
const totalTokens =
|
|
22
|
+
const blockTokenLens = blocks.map((b) => tokenize(b.text).length);
|
|
23
|
+
const totalTokens = blockTokenLens.reduce((sum, len) => sum + len, 0);
|
|
21
24
|
const avgBlockLen = blocks.length ? totalTokens / blocks.length : 1;
|
|
25
|
+
const agents = normalizeAgents(opts.agents);
|
|
22
26
|
const meta = {
|
|
23
|
-
version:
|
|
27
|
+
version: 3,
|
|
24
28
|
stats: {
|
|
25
|
-
docs:
|
|
29
|
+
docs: normalizedDocs.length,
|
|
26
30
|
blocks: blocks.length,
|
|
27
31
|
terms: lexicon.length,
|
|
28
32
|
avgBlockLen,
|
|
29
33
|
},
|
|
34
|
+
...(agents ? { agents } : {}),
|
|
30
35
|
};
|
|
31
|
-
// Persist blocks as objects to optionally carry heading/docId
|
|
36
|
+
// Persist blocks as objects to optionally carry heading/docId/token length.
|
|
32
37
|
const blocksPayload = blocks.map((b, i) => ({
|
|
33
38
|
text: b.text,
|
|
34
39
|
heading: b.heading ?? null,
|
|
35
|
-
docId:
|
|
40
|
+
docId: normalizedDocs[i]?.id ?? null,
|
|
41
|
+
namespace: normalizedDocs[i]?.namespace ?? null,
|
|
42
|
+
len: blockTokenLens[i] ?? 0,
|
|
36
43
|
}));
|
|
37
44
|
// Encode sections
|
|
38
45
|
const enc = getTextEncoder();
|
|
39
46
|
const metaBytes = enc.encode(JSON.stringify(meta));
|
|
40
47
|
const lexBytes = enc.encode(JSON.stringify(lexicon));
|
|
41
48
|
const blocksBytes = enc.encode(JSON.stringify(blocksPayload));
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
const semanticEnabled = Boolean(opts.semantic?.enabled);
|
|
50
|
+
const semanticSection = semanticEnabled && opts.semantic
|
|
51
|
+
? buildSemanticSection(blocks.length, opts.semantic)
|
|
52
|
+
: undefined;
|
|
53
|
+
const semBytes = semanticSection
|
|
54
|
+
? enc.encode(JSON.stringify(semanticSection.semJson))
|
|
55
|
+
: undefined;
|
|
56
|
+
const semBlob = semanticSection?.semBlob;
|
|
57
|
+
const totalLength = 4 +
|
|
58
|
+
metaBytes.length +
|
|
59
|
+
4 +
|
|
60
|
+
lexBytes.length +
|
|
61
|
+
4 +
|
|
62
|
+
postings.length * 4 +
|
|
63
|
+
4 +
|
|
64
|
+
blocksBytes.length +
|
|
65
|
+
(semanticEnabled && semBytes && semBlob
|
|
66
|
+
? 4 + semBytes.length + 4 + semBlob.length
|
|
67
|
+
: 0);
|
|
46
68
|
const out = new Uint8Array(totalLength);
|
|
47
69
|
const dv = new DataView(out.buffer);
|
|
48
70
|
let offset = 0;
|
|
@@ -67,8 +89,100 @@ export async function buildPack(docs) {
|
|
|
67
89
|
dv.setUint32(offset, blocksBytes.length, true);
|
|
68
90
|
offset += 4;
|
|
69
91
|
out.set(blocksBytes, offset);
|
|
92
|
+
offset += blocksBytes.length;
|
|
93
|
+
if (semanticEnabled && semBytes && semBlob) {
|
|
94
|
+
dv.setUint32(offset, semBytes.length, true);
|
|
95
|
+
offset += 4;
|
|
96
|
+
out.set(semBytes, offset);
|
|
97
|
+
offset += semBytes.length;
|
|
98
|
+
dv.setUint32(offset, semBlob.length, true);
|
|
99
|
+
offset += 4;
|
|
100
|
+
out.set(semBlob, offset);
|
|
101
|
+
}
|
|
70
102
|
return out;
|
|
71
103
|
}
|
|
104
|
+
function normalizeAgents(input) {
|
|
105
|
+
if (!input)
|
|
106
|
+
return undefined;
|
|
107
|
+
const registry = Array.isArray(input)
|
|
108
|
+
? { version: 1, agents: input }
|
|
109
|
+
: input;
|
|
110
|
+
validateAgentRegistry(registry);
|
|
111
|
+
return registry;
|
|
112
|
+
}
|
|
113
|
+
function buildSemanticSection(blockCount, semantic) {
|
|
114
|
+
const { embeddings } = semantic;
|
|
115
|
+
if (!Array.isArray(embeddings) || embeddings.length !== blockCount) {
|
|
116
|
+
throw new Error(`semantic.embeddings must be provided with one embedding per block (expected ${blockCount}).`);
|
|
117
|
+
}
|
|
118
|
+
const quantizationType = semantic.quantization?.type ?? 'int8_l2norm';
|
|
119
|
+
if (quantizationType !== 'int8_l2norm') {
|
|
120
|
+
throw new Error(`Unsupported semantic quantization type: ${quantizationType}`);
|
|
121
|
+
}
|
|
122
|
+
const dims = embeddings[0]?.length ?? 0;
|
|
123
|
+
if (!dims)
|
|
124
|
+
throw new Error('semantic.embeddings must contain vectors with non-zero dimensions.');
|
|
125
|
+
const vecs = new Int8Array(embeddings.length * dims);
|
|
126
|
+
const scales = new Uint16Array(embeddings.length);
|
|
127
|
+
for (let i = 0; i < embeddings.length; i++) {
|
|
128
|
+
const embedding = embeddings[i];
|
|
129
|
+
if (!(embedding instanceof Float32Array)) {
|
|
130
|
+
throw new Error(`semantic.embeddings[${i}] must be a Float32Array.`);
|
|
131
|
+
}
|
|
132
|
+
if (embedding.length !== dims) {
|
|
133
|
+
throw new Error(`semantic.embeddings[${i}] dims mismatch: expected ${dims}, got ${embedding.length}.`);
|
|
134
|
+
}
|
|
135
|
+
const { q, scale } = quantizeEmbeddingInt8L2Norm(embedding);
|
|
136
|
+
vecs.set(q, i * dims);
|
|
137
|
+
scales[i] = encodeScaleF16(scale);
|
|
138
|
+
}
|
|
139
|
+
const vecByteOffset = 0;
|
|
140
|
+
const vecByteLength = vecs.byteLength;
|
|
141
|
+
const scalesByteOffset = vecByteLength;
|
|
142
|
+
const scalesByteLength = scales.byteLength;
|
|
143
|
+
const semBlob = new Uint8Array(vecByteLength + scalesByteLength);
|
|
144
|
+
semBlob.set(new Uint8Array(vecs.buffer, vecs.byteOffset, vecByteLength), vecByteOffset);
|
|
145
|
+
semBlob.set(new Uint8Array(scales.buffer, scales.byteOffset, scalesByteLength), scalesByteOffset);
|
|
146
|
+
const semJson = {
|
|
147
|
+
version: 1,
|
|
148
|
+
modelId: semantic.modelId,
|
|
149
|
+
dims,
|
|
150
|
+
encoding: 'int8_l2norm',
|
|
151
|
+
perVectorScale: true,
|
|
152
|
+
blocks: {
|
|
153
|
+
vectors: { byteOffset: vecByteOffset, length: vecs.length },
|
|
154
|
+
scales: {
|
|
155
|
+
byteOffset: scalesByteOffset,
|
|
156
|
+
length: scales.length,
|
|
157
|
+
encoding: 'float16',
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
return { semJson, semBlob };
|
|
162
|
+
}
|
|
163
|
+
function validateDocs(docs) {
|
|
164
|
+
if (!Array.isArray(docs)) {
|
|
165
|
+
throw new Error('buildPack expects an array of docs: [{ text, id?, heading?, namespace? }, ...]');
|
|
166
|
+
}
|
|
167
|
+
return docs.map((doc, i) => {
|
|
168
|
+
if (!doc || typeof doc !== 'object') {
|
|
169
|
+
throw new Error(`Invalid doc at index ${i}: expected an object with a string "text" field.`);
|
|
170
|
+
}
|
|
171
|
+
if (typeof doc.text !== 'string' || !doc.text.trim()) {
|
|
172
|
+
throw new Error(`Invalid doc at index ${i}: "text" must be a non-empty string.`);
|
|
173
|
+
}
|
|
174
|
+
if (doc.id !== undefined && typeof doc.id !== 'string') {
|
|
175
|
+
throw new Error(`Invalid doc at index ${i}: "id" must be a string when provided.`);
|
|
176
|
+
}
|
|
177
|
+
if (doc.heading !== undefined && typeof doc.heading !== 'string') {
|
|
178
|
+
throw new Error(`Invalid doc at index ${i}: "heading" must be a string when provided.`);
|
|
179
|
+
}
|
|
180
|
+
if (doc.namespace !== undefined && typeof doc.namespace !== 'string') {
|
|
181
|
+
throw new Error(`Invalid doc at index ${i}: "namespace" must be a string when provided.`);
|
|
182
|
+
}
|
|
183
|
+
return doc;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
72
186
|
/** Strip Markdown syntax with lightweight regexes (no deps). */
|
|
73
187
|
function stripMd(md) {
|
|
74
188
|
let text = md.replace(/```[\s\S]*?```/g, ' ');
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
export { mountPack } from './pack.js';
|
|
2
|
-
export { query } from './query.js';
|
|
1
|
+
export { mountPack, hasSemantic } from './pack.js';
|
|
2
|
+
export { query, lexConfidence, validateQueryOptions, 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';
|
|
6
|
+
export { listAgents, getAgent, resolveAgent, buildSystemPrompt, isToolAllowed, assertToolAllowed, validateAgentRegistry, validateAgentDefinition, } from './agent.js';
|
|
5
7
|
export type { MountOptions, PackMeta, Pack } from './pack.js';
|
|
6
8
|
export type { QueryOptions, Hit } from './query.js';
|
|
7
9
|
export type { ContextPatch } from './patch.js';
|
|
10
|
+
export type { BuildInputDoc, BuildPackOptions } from './builder.js';
|
|
11
|
+
export type { AgentPromptTemplate, AgentToolPolicy, AgentRetrievalDefaults, AgentDefinitionV1, AgentRegistry, ResolveAgentInput, ResolvedAgent, } from './agent.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
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, validateQueryOptions, 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';
|
|
7
|
+
export { listAgents, getAgent, resolveAgent, buildSystemPrompt, isToolAllowed, assertToolAllowed, validateAgentRegistry, validateAgentDefinition, } from './agent.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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AgentRegistry } from './agent.js';
|
|
1
2
|
export type MountOptions = {
|
|
2
3
|
src: string | ArrayBufferLike | Uint8Array;
|
|
3
4
|
};
|
|
@@ -9,6 +10,7 @@ export type PackMeta = {
|
|
|
9
10
|
terms: number;
|
|
10
11
|
avgBlockLen?: number;
|
|
11
12
|
};
|
|
13
|
+
agents?: AgentRegistry;
|
|
12
14
|
};
|
|
13
15
|
export type Pack = {
|
|
14
16
|
meta: PackMeta;
|
|
@@ -17,5 +19,17 @@ export type Pack = {
|
|
|
17
19
|
blocks: string[];
|
|
18
20
|
headings?: (string | null)[];
|
|
19
21
|
docIds?: (string | null)[];
|
|
22
|
+
namespaces?: (string | null)[];
|
|
23
|
+
blockTokenLens?: number[];
|
|
24
|
+
semantic?: {
|
|
25
|
+
version: 1;
|
|
26
|
+
modelId: string;
|
|
27
|
+
dims: number;
|
|
28
|
+
encoding: 'int8_l2norm';
|
|
29
|
+
perVectorScale: boolean;
|
|
30
|
+
vecs: Int8Array;
|
|
31
|
+
scales?: Uint16Array;
|
|
32
|
+
};
|
|
20
33
|
};
|
|
34
|
+
export declare function hasSemantic(pack: Pack): boolean;
|
|
21
35
|
export declare function mountPack(opts: MountOptions): Promise<Pack>;
|
package/dist/pack.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
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
|
+
import { validateAgentRegistry } from './agent.js';
|
|
11
|
+
export function hasSemantic(pack) {
|
|
12
|
+
return Boolean(pack.semantic && pack.semantic.dims > 0 && pack.semantic.vecs.length > 0);
|
|
13
|
+
}
|
|
10
14
|
export async function mountPack(opts) {
|
|
11
15
|
const buf = await resolveToBuffer(opts.src);
|
|
12
16
|
const dv = new DataView(buf);
|
|
@@ -18,6 +22,9 @@ export async function mountPack(opts) {
|
|
|
18
22
|
const metaJson = dec.decode(new Uint8Array(buf, offset, metaLen));
|
|
19
23
|
offset += metaLen;
|
|
20
24
|
const meta = JSON.parse(metaJson);
|
|
25
|
+
if (meta.agents) {
|
|
26
|
+
validateAgentRegistry(meta.agents);
|
|
27
|
+
}
|
|
21
28
|
// lexicon
|
|
22
29
|
const lexLen = dv.getUint32(offset, true);
|
|
23
30
|
offset += 4;
|
|
@@ -33,14 +40,17 @@ export async function mountPack(opts) {
|
|
|
33
40
|
postings[i] = dv.getUint32(offset, true);
|
|
34
41
|
offset += 4;
|
|
35
42
|
}
|
|
36
|
-
// blocks (v1: string[]; v2: {text, heading?, docId?}[])
|
|
43
|
+
// blocks (v1: string[]; v2/v3: {text, heading?, docId?, namespace?, len?}[])
|
|
37
44
|
const blocksLen = dv.getUint32(offset, true);
|
|
38
45
|
offset += 4;
|
|
39
46
|
const blocksJson = dec.decode(new Uint8Array(buf, offset, blocksLen));
|
|
47
|
+
offset += blocksLen;
|
|
40
48
|
const parsed = JSON.parse(blocksJson);
|
|
41
49
|
let blocks = [];
|
|
42
50
|
let headings;
|
|
43
51
|
let docIds;
|
|
52
|
+
let namespaces;
|
|
53
|
+
let blockTokenLens;
|
|
44
54
|
if (Array.isArray(parsed) && parsed.length && typeof parsed[0] === 'string') {
|
|
45
55
|
// v1
|
|
46
56
|
blocks = parsed;
|
|
@@ -49,26 +59,81 @@ export async function mountPack(opts) {
|
|
|
49
59
|
blocks = [];
|
|
50
60
|
headings = [];
|
|
51
61
|
docIds = [];
|
|
62
|
+
namespaces = [];
|
|
63
|
+
blockTokenLens = [];
|
|
52
64
|
for (const it of parsed) {
|
|
53
65
|
if (it && typeof it === 'object') {
|
|
54
66
|
blocks.push(String(it.text ?? ''));
|
|
55
67
|
headings.push(it.heading ?? null);
|
|
56
68
|
docIds.push(it.docId ?? null);
|
|
69
|
+
namespaces.push(it.namespace ?? null);
|
|
70
|
+
blockTokenLens.push(typeof it.len === 'number' ? it.len : 0);
|
|
57
71
|
}
|
|
58
72
|
else {
|
|
59
73
|
blocks.push(String(it ?? ''));
|
|
60
74
|
headings.push(null);
|
|
61
75
|
docIds.push(null);
|
|
76
|
+
namespaces.push(null);
|
|
77
|
+
blockTokenLens.push(0);
|
|
62
78
|
}
|
|
63
79
|
}
|
|
64
80
|
}
|
|
65
81
|
else {
|
|
66
82
|
blocks = [];
|
|
67
83
|
}
|
|
68
|
-
|
|
84
|
+
let semantic;
|
|
85
|
+
if (offset < buf.byteLength) {
|
|
86
|
+
const semLen = dv.getUint32(offset, true);
|
|
87
|
+
offset += 4;
|
|
88
|
+
const semJson = dec.decode(new Uint8Array(buf, offset, semLen));
|
|
89
|
+
offset += semLen;
|
|
90
|
+
const sem = JSON.parse(semJson);
|
|
91
|
+
const semBlobLen = dv.getUint32(offset, true);
|
|
92
|
+
offset += 4;
|
|
93
|
+
const semBlob = new Uint8Array(buf, offset, semBlobLen);
|
|
94
|
+
semantic = parseSemanticSection(sem, semBlob);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
meta,
|
|
98
|
+
lexicon,
|
|
99
|
+
postings,
|
|
100
|
+
blocks,
|
|
101
|
+
headings,
|
|
102
|
+
docIds,
|
|
103
|
+
namespaces,
|
|
104
|
+
blockTokenLens,
|
|
105
|
+
semantic,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function parseSemanticSection(sem, blob) {
|
|
109
|
+
const vectors = sem?.blocks?.vectors;
|
|
110
|
+
const scales = sem?.blocks?.scales;
|
|
111
|
+
const vecs = new Int8Array(blob.buffer, blob.byteOffset + Number(vectors?.byteOffset ?? 0), Number(vectors?.length ?? 0));
|
|
112
|
+
let scaleView;
|
|
113
|
+
if (scales) {
|
|
114
|
+
const scaleLen = Number(scales.length ?? 0);
|
|
115
|
+
const scaleOffset = Number(scales.byteOffset ?? 0);
|
|
116
|
+
const dv = new DataView(blob.buffer, blob.byteOffset + scaleOffset, scaleLen * 2);
|
|
117
|
+
scaleView = new Uint16Array(scaleLen);
|
|
118
|
+
for (let i = 0; i < scaleLen; i++) {
|
|
119
|
+
scaleView[i] = dv.getUint16(i * 2, true);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
version: 1,
|
|
124
|
+
modelId: String(sem?.modelId ?? ''),
|
|
125
|
+
dims: Number(sem?.dims ?? 0),
|
|
126
|
+
encoding: 'int8_l2norm',
|
|
127
|
+
perVectorScale: Boolean(sem?.perVectorScale),
|
|
128
|
+
vecs,
|
|
129
|
+
scales: scaleView,
|
|
130
|
+
};
|
|
69
131
|
}
|
|
70
132
|
async function resolveToBuffer(src) {
|
|
71
133
|
if (typeof src === 'string') {
|
|
134
|
+
if (isNodeRuntime() && isLikelyLocalPath(src)) {
|
|
135
|
+
return await readLocalFileAsBuffer(src);
|
|
136
|
+
}
|
|
72
137
|
const res = await fetch(src);
|
|
73
138
|
return await res.arrayBuffer();
|
|
74
139
|
}
|
|
@@ -81,3 +146,30 @@ async function resolveToBuffer(src) {
|
|
|
81
146
|
}
|
|
82
147
|
return src;
|
|
83
148
|
}
|
|
149
|
+
function isNodeRuntime() {
|
|
150
|
+
const p = globalThis
|
|
151
|
+
.process;
|
|
152
|
+
return !!p?.versions?.node;
|
|
153
|
+
}
|
|
154
|
+
function isLikelyLocalPath(value) {
|
|
155
|
+
if (value.startsWith('file://'))
|
|
156
|
+
return true;
|
|
157
|
+
if (value.startsWith('./') ||
|
|
158
|
+
value.startsWith('../') ||
|
|
159
|
+
value.startsWith('/') ||
|
|
160
|
+
value.startsWith('~'))
|
|
161
|
+
return true;
|
|
162
|
+
if (/^[A-Za-z]:[\\/]/.test(value))
|
|
163
|
+
return true; // Windows absolute path
|
|
164
|
+
if (/^[A-Za-z][A-Za-z\d+.-]*:/.test(value))
|
|
165
|
+
return false; // URL scheme
|
|
166
|
+
return true; // plain relative path like "knowledge.knolo"
|
|
167
|
+
}
|
|
168
|
+
async function readLocalFileAsBuffer(pathOrFileUrl) {
|
|
169
|
+
const { readFile } = await import('node:fs/promises');
|
|
170
|
+
const filePath = pathOrFileUrl.startsWith('file://')
|
|
171
|
+
? decodeURIComponent(new URL(pathOrFileUrl).pathname)
|
|
172
|
+
: pathOrFileUrl;
|
|
173
|
+
const data = await readFile(filePath);
|
|
174
|
+
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
175
|
+
}
|
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,41 @@
|
|
|
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 validateQueryOptions(opts?: QueryOptions): void;
|
|
30
|
+
export declare function validateSemanticQueryOptions(options?: QueryOptions["semantic"]): void;
|
|
6
31
|
export type Hit = {
|
|
7
32
|
blockId: number;
|
|
8
33
|
score: number;
|
|
9
34
|
text: string;
|
|
10
35
|
source?: string;
|
|
36
|
+
namespace?: string;
|
|
11
37
|
};
|
|
12
38
|
export declare function query(pack: Pack, q: string, opts?: QueryOptions): Hit[];
|
|
39
|
+
export declare function lexConfidence(hits: Array<{
|
|
40
|
+
score: number;
|
|
41
|
+
}>): number;
|