tool-prune 0.1.0 → 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/README.md +25 -8
- package/index.d.ts +21 -2
- package/lib/router.js +186 -10
- package/lib/turboquant.js +186 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
# tool-prune
|
|
2
2
|
|
|
3
|
-
Calibrated tool selection and schema pruning for AI agents.
|
|
3
|
+
Calibrated tool selection and schema pruning for AI agents. Dual-engine: zero-dependency offline TurboQuant or TypeSafe System One.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install tool-prune
|
|
7
|
+
|
|
8
|
+
# Optional SIMD acceleration
|
|
9
|
+
npm install turboquant-search
|
|
10
|
+
|
|
11
|
+
# Optional: TypeSafe API key for cloud reasoning
|
|
12
|
+
export TYPESAFE_API_KEY="apikey_..."
|
|
7
13
|
```
|
|
8
14
|
|
|
9
15
|
## Quick start
|
|
@@ -17,18 +23,21 @@ const tools = {
|
|
|
17
23
|
webSearch: 'Search public web for documentation or articles'
|
|
18
24
|
};
|
|
19
25
|
|
|
26
|
+
// Works offline out of the box with TurboQuant:
|
|
20
27
|
const match = await prune('what tables exist in the db?', tools);
|
|
21
|
-
console.log(match.tool);
|
|
22
|
-
console.log(match.
|
|
28
|
+
console.log(match.tool); // 'runQuery'
|
|
29
|
+
console.log(match.engine); // 'turboquant'
|
|
23
30
|
```
|
|
24
31
|
|
|
25
|
-
`prune()` narrows schemas
|
|
32
|
+
`prune()` narrows schemas offline via TurboQuant by default, or routes to TypeSafe System One when `TYPESAFE_API_KEY` is present. That's the whole API.
|
|
26
33
|
|
|
27
34
|
## Schema pruning for LLMs
|
|
28
35
|
|
|
29
36
|
```js
|
|
30
37
|
const router = prune(tools);
|
|
31
|
-
|
|
38
|
+
|
|
39
|
+
// Auto-selects candidate schemas dynamically (or pass { k: 5 }):
|
|
40
|
+
const topTools = await router.filter(userPrompt);
|
|
32
41
|
|
|
33
42
|
const response = await llm.chat({
|
|
34
43
|
tools: topTools,
|
|
@@ -36,7 +45,7 @@ const response = await llm.chat({
|
|
|
36
45
|
});
|
|
37
46
|
```
|
|
38
47
|
|
|
39
|
-
|
|
48
|
+
Cuts prompt tokens by up to 92% and eliminates context confusion without losing tools.
|
|
40
49
|
|
|
41
50
|
## Fast-path direct dispatch
|
|
42
51
|
|
|
@@ -50,14 +59,22 @@ const result = await router.dispatch('read ./package.json', {
|
|
|
50
59
|
|
|
51
60
|
Runs deterministic handlers in under 160ms with zero token cost.
|
|
52
61
|
|
|
62
|
+
## Dual engine
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
const localMatch = await prune(query, tools, { engine: 'turboquant' });
|
|
66
|
+
const cloudMatch = await prune(query, tools, { engine: 'typesafe', apiKey: '...' });
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- **turboquant**: 100% offline, zero network, zero dependencies. Uses `turboquant-search` (WASM SIMD) if installed, with built-in FWHT fallback.
|
|
70
|
+
- **typesafe**: Cloud System One reasoning (Jev). 100% Top-1 accuracy on subtle distractors with calibrated probabilities.
|
|
71
|
+
|
|
53
72
|
## Demo
|
|
54
73
|
|
|
55
74
|
```bash
|
|
56
75
|
npm run demo
|
|
57
76
|
```
|
|
58
77
|
|
|
59
|
-
Runs live quickstart routing with your `TYPESAFE_API_KEY`.
|
|
60
|
-
|
|
61
78
|
## License
|
|
62
79
|
|
|
63
80
|
MIT © [Hemanth.HM](https://h3manth.com)
|
package/index.d.ts
CHANGED
|
@@ -13,13 +13,25 @@ export interface ToolPruneOptions {
|
|
|
13
13
|
apiKey?: string;
|
|
14
14
|
endpoint?: string;
|
|
15
15
|
model?: string;
|
|
16
|
+
engine?: 'typesafe' | 'turboquant';
|
|
16
17
|
threshold?: number;
|
|
17
|
-
topK?: number;
|
|
18
|
+
topK?: number | 'auto';
|
|
19
|
+
k?: number | 'auto';
|
|
20
|
+
auto?: boolean;
|
|
21
|
+
minK?: number;
|
|
22
|
+
maxK?: number;
|
|
23
|
+
minScore?: number;
|
|
24
|
+
minProbability?: number;
|
|
25
|
+
relativeThreshold?: number;
|
|
26
|
+
cliffRatio?: number;
|
|
27
|
+
dominantMargin?: number;
|
|
28
|
+
allowEmpty?: boolean;
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
export interface CandidateTool {
|
|
21
32
|
name: string;
|
|
22
33
|
probability: number;
|
|
34
|
+
score?: number;
|
|
23
35
|
tool?: any;
|
|
24
36
|
}
|
|
25
37
|
|
|
@@ -28,8 +40,11 @@ export interface SelectionResult {
|
|
|
28
40
|
confidence: number;
|
|
29
41
|
probability: number;
|
|
30
42
|
topK: CandidateTool[];
|
|
43
|
+
autoSelected: CandidateTool[];
|
|
44
|
+
autoTools: any[];
|
|
31
45
|
requiresGeneration: number;
|
|
32
46
|
latency: number;
|
|
47
|
+
engine?: string;
|
|
33
48
|
usage?: {
|
|
34
49
|
input_tokens: number;
|
|
35
50
|
output_tokens: number;
|
|
@@ -37,10 +52,13 @@ export interface SelectionResult {
|
|
|
37
52
|
raw?: any;
|
|
38
53
|
}
|
|
39
54
|
|
|
55
|
+
export function autoSelectCandidates(candidates: CandidateTool[], options?: Partial<ToolPruneOptions>): CandidateTool[];
|
|
56
|
+
|
|
40
57
|
export class ToolPruner {
|
|
41
58
|
constructor(tools: ToolInput, options?: ToolPruneOptions);
|
|
42
59
|
select(query: string | Record<string, any>, options?: Partial<ToolPruneOptions>): Promise<SelectionResult>;
|
|
43
|
-
filter(query: string | Record<string, any>, options?:
|
|
60
|
+
filter(query: string | Record<string, any>, options?: Partial<ToolPruneOptions>): Promise<any[]>;
|
|
61
|
+
auto(query: string | Record<string, any>, options?: Partial<ToolPruneOptions>): Promise<any[]>;
|
|
44
62
|
dispatch<T = any>(
|
|
45
63
|
query: string | Record<string, any>,
|
|
46
64
|
handlers: Record<string, (query: any, selection: SelectionResult) => Promise<T> | T>,
|
|
@@ -51,3 +69,4 @@ export class ToolPruner {
|
|
|
51
69
|
export default function toolPrune(query: string, tools: ToolInput, options?: ToolPruneOptions): Promise<SelectionResult>;
|
|
52
70
|
export default function toolPrune(tools: ToolInput, options?: ToolPruneOptions): ToolPruner;
|
|
53
71
|
|
|
72
|
+
|
package/lib/router.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { TurboQuantEngine } from './turboquant.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Normalize diverse tool definitions (Dict, Array, MCP Schema) into a clean criteria map.
|
|
3
5
|
*/
|
|
@@ -39,15 +41,36 @@ export class ToolPruner {
|
|
|
39
41
|
this.endpoint = options.endpoint || 'https://api.typesafe.ai/v1/systemone';
|
|
40
42
|
this.model = options.model || 'jev-latest';
|
|
41
43
|
this.threshold = options.threshold ?? 0.85;
|
|
42
|
-
this.defaultTopK = options.topK ??
|
|
44
|
+
this.defaultTopK = options.topK ?? 'auto';
|
|
45
|
+
this.requestedEngine = options.engine;
|
|
46
|
+
this._tqEngine = null;
|
|
47
|
+
this._wasmEngine = null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Determine engine: explicit option, or fallback to turboquant if no API key.
|
|
52
|
+
*/
|
|
53
|
+
getEngine(options = {}) {
|
|
54
|
+
const eng = options.engine || this.requestedEngine;
|
|
55
|
+
if (eng) return eng;
|
|
56
|
+
return this.apiKey ? 'typesafe' : 'turboquant';
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
/**
|
|
46
60
|
* Select the most appropriate tool for a given query with calibrated confidence.
|
|
47
61
|
*/
|
|
48
62
|
async select(query, options = {}) {
|
|
49
|
-
|
|
50
|
-
|
|
63
|
+
const engine = this.getEngine(options);
|
|
64
|
+
if (engine === 'turboquant') {
|
|
65
|
+
return this._selectTurboQuant(query, options);
|
|
66
|
+
}
|
|
67
|
+
return this._selectTypeSafe(query, options);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async _selectTypeSafe(query, options = {}) {
|
|
71
|
+
const key = options.apiKey || this.apiKey;
|
|
72
|
+
if (!key) {
|
|
73
|
+
throw new Error("TypeSafe API key required. Set TYPESAFE_API_KEY, pass { apiKey }, or use { engine: 'turboquant' }.");
|
|
51
74
|
}
|
|
52
75
|
|
|
53
76
|
const state = typeof query === 'string' ? { intent: query } : query;
|
|
@@ -71,7 +94,7 @@ export class ToolPruner {
|
|
|
71
94
|
const res = await fetch(this.endpoint, {
|
|
72
95
|
method: 'POST',
|
|
73
96
|
headers: {
|
|
74
|
-
Authorization: `Bearer ${
|
|
97
|
+
Authorization: `Bearer ${key}`,
|
|
75
98
|
'Content-Type': 'application/json'
|
|
76
99
|
},
|
|
77
100
|
body: JSON.stringify(body)
|
|
@@ -88,14 +111,19 @@ export class ToolPruner {
|
|
|
88
111
|
const toolAnswer = data.answers?.tool;
|
|
89
112
|
const probs = toolAnswer?.probabilities || {};
|
|
90
113
|
const sorted = Object.entries(probs).sort((a, b) => b[1] - a[1]);
|
|
91
|
-
const
|
|
92
|
-
const topK = sorted.slice(0, topKCount).map(([name, p]) => ({
|
|
114
|
+
const allCandidates = sorted.map(([name, p]) => ({
|
|
93
115
|
name,
|
|
94
116
|
probability: p,
|
|
95
117
|
tool: this.registry.get(name)
|
|
96
118
|
}));
|
|
97
119
|
|
|
98
|
-
const
|
|
120
|
+
const autoSelected = autoSelectCandidates(allCandidates, options);
|
|
121
|
+
const autoTools = autoSelected.map(c => c.tool || c.name);
|
|
122
|
+
|
|
123
|
+
const k = typeof options.topK === 'number' ? options.topK : (typeof this.defaultTopK === 'number' ? this.defaultTopK : null);
|
|
124
|
+
const topK = typeof k === 'number' ? allCandidates.slice(0, k) : allCandidates.slice(0, Math.max(3, autoSelected.length));
|
|
125
|
+
|
|
126
|
+
const selectedName = toolAnswer?.choice || topK[0]?.name || '';
|
|
99
127
|
const probability = probs[selectedName] ?? 0;
|
|
100
128
|
const confidence = toolAnswer?.confidence ?? probability;
|
|
101
129
|
const requiresGeneration = data.answers?.requires_generation?.probability ?? 0;
|
|
@@ -105,20 +133,107 @@ export class ToolPruner {
|
|
|
105
133
|
confidence,
|
|
106
134
|
probability,
|
|
107
135
|
topK,
|
|
136
|
+
autoSelected,
|
|
137
|
+
autoTools,
|
|
108
138
|
requiresGeneration,
|
|
109
139
|
latency,
|
|
140
|
+
engine: 'typesafe',
|
|
110
141
|
usage: data.usage,
|
|
111
142
|
raw: data
|
|
112
143
|
};
|
|
113
144
|
}
|
|
114
145
|
|
|
146
|
+
async _selectTurboQuant(query, options = {}) {
|
|
147
|
+
const qStr = typeof query === 'string' ? query : (query.intent || query.query || JSON.stringify(query));
|
|
148
|
+
const start = performance.now();
|
|
149
|
+
|
|
150
|
+
// Check if turboquant-search (WASM) is available
|
|
151
|
+
if (!this._tqEngine) {
|
|
152
|
+
try {
|
|
153
|
+
const { TurboSearch } = await import('turboquant-search');
|
|
154
|
+
const data = Array.from(this.registry.values()).map(t => ({
|
|
155
|
+
name: t.name,
|
|
156
|
+
description: t.description || '',
|
|
157
|
+
criteria: t.criteria || ''
|
|
158
|
+
}));
|
|
159
|
+
this._wasmEngine = await TurboSearch.from(data, {
|
|
160
|
+
fields: ['name', 'description', 'criteria'],
|
|
161
|
+
dim: 384,
|
|
162
|
+
bits: 3
|
|
163
|
+
});
|
|
164
|
+
} catch {
|
|
165
|
+
// Fallback to built-in zero-dependency TurboQuant engine
|
|
166
|
+
this._tqEngine = new TurboQuantEngine();
|
|
167
|
+
const toolsMap = {};
|
|
168
|
+
for (const [name, tool] of this.registry.entries()) {
|
|
169
|
+
toolsMap[name] = tool;
|
|
170
|
+
}
|
|
171
|
+
this._tqEngine.fit(toolsMap);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const k = typeof options.topK === 'number' ? options.topK : (typeof this.defaultTopK === 'number' ? this.defaultTopK : null);
|
|
176
|
+
const poolSize = Math.max(k || 3, 10, this.registry.size);
|
|
177
|
+
let allCandidates = [];
|
|
178
|
+
|
|
179
|
+
if (this._wasmEngine) {
|
|
180
|
+
const results = await this._wasmEngine.search(qStr, { topK: poolSize });
|
|
181
|
+
allCandidates = results.map(r => ({
|
|
182
|
+
name: r.data?.name || '',
|
|
183
|
+
probability: r.score || 0,
|
|
184
|
+
score: r.score || 0,
|
|
185
|
+
tool: this.registry.get(r.data?.name)
|
|
186
|
+
}));
|
|
187
|
+
} else {
|
|
188
|
+
const rawResults = this._tqEngine.search(qStr, poolSize);
|
|
189
|
+
allCandidates = rawResults.map(r => ({
|
|
190
|
+
name: r.name,
|
|
191
|
+
probability: r.probability,
|
|
192
|
+
score: r.score,
|
|
193
|
+
tool: this.registry.get(r.name)
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const latency = performance.now() - start;
|
|
198
|
+
const top1 = allCandidates[0];
|
|
199
|
+
const autoSelected = autoSelectCandidates(allCandidates, options);
|
|
200
|
+
const autoTools = autoSelected.map(c => c.tool || c.name);
|
|
201
|
+
const topK = typeof k === 'number' ? allCandidates.slice(0, k) : allCandidates.slice(0, Math.max(3, autoSelected.length));
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
tool: top1?.name || '',
|
|
205
|
+
confidence: top1?.probability || 0,
|
|
206
|
+
probability: top1?.probability || 0,
|
|
207
|
+
topK,
|
|
208
|
+
autoSelected,
|
|
209
|
+
autoTools,
|
|
210
|
+
requiresGeneration: 0,
|
|
211
|
+
latency,
|
|
212
|
+
engine: 'turboquant'
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
115
216
|
/**
|
|
116
|
-
* Filter tool collection down to
|
|
217
|
+
* Filter tool collection down to relevant candidates to prune LLM prompt bloat.
|
|
218
|
+
* If k or topK is a number, returns that fixed number of candidates.
|
|
219
|
+
* Otherwise (default or k='auto'), automatically selects the candidates based on score drop-off.
|
|
117
220
|
*/
|
|
118
221
|
async filter(query, options = {}) {
|
|
222
|
+
const k = options.k ?? options.topK ?? this.defaultTopK;
|
|
223
|
+
if (typeof k === 'number' && k > 0) {
|
|
224
|
+
const res = await this.select(query, { ...options, topK: k });
|
|
225
|
+
return res.topK.slice(0, k).map(item => item.tool || item.name);
|
|
226
|
+
}
|
|
227
|
+
const res = await this.select(query, options);
|
|
228
|
+
return res.autoTools;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Automatically select the optimal candidate tools based on score distribution.
|
|
233
|
+
*/
|
|
234
|
+
async auto(query, options = {}) {
|
|
119
235
|
const res = await this.select(query, options);
|
|
120
|
-
|
|
121
|
-
return res.topK.slice(0, k).map(item => item.tool || item.name);
|
|
236
|
+
return res.autoTools;
|
|
122
237
|
}
|
|
123
238
|
|
|
124
239
|
/**
|
|
@@ -140,6 +255,61 @@ export class ToolPruner {
|
|
|
140
255
|
}
|
|
141
256
|
}
|
|
142
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Automatically select the most relevant tool candidates based on score distribution,
|
|
260
|
+
* cliff / elbow drop-off, and relevance floors.
|
|
261
|
+
*/
|
|
262
|
+
export function autoSelectCandidates(candidates, options = {}) {
|
|
263
|
+
if (!candidates || candidates.length === 0) return [];
|
|
264
|
+
|
|
265
|
+
const maxK = options.maxK ?? 5;
|
|
266
|
+
const minK = options.minK ?? (options.allowEmpty ? 0 : 1);
|
|
267
|
+
const minScore = options.minScore ?? 0.12;
|
|
268
|
+
const minProb = options.minProbability ?? options.minProb ?? 0.20;
|
|
269
|
+
const relativeThreshold = options.relativeThreshold ?? 0.70;
|
|
270
|
+
const cliffRatio = options.cliffRatio ?? 0.75;
|
|
271
|
+
const dominantMargin = options.dominantMargin ?? 0.14;
|
|
272
|
+
|
|
273
|
+
const top1 = candidates[0];
|
|
274
|
+
const hasScore = typeof top1.score === 'number';
|
|
275
|
+
const topVal = hasScore ? top1.score : top1.probability;
|
|
276
|
+
const floorVal = hasScore ? minScore : minProb;
|
|
277
|
+
|
|
278
|
+
if (topVal < floorVal) {
|
|
279
|
+
return minK > 0 ? candidates.slice(0, minK) : [];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const selected = [top1];
|
|
283
|
+
|
|
284
|
+
for (let i = 1; i < Math.min(candidates.length, maxK); i++) {
|
|
285
|
+
const curr = candidates[i];
|
|
286
|
+
const prev = candidates[i - 1];
|
|
287
|
+
|
|
288
|
+
if (hasScore) {
|
|
289
|
+
if (curr.score < minScore) break;
|
|
290
|
+
// Dominant lead: top1 is strong and clearly ahead
|
|
291
|
+
if (top1.score >= 0.35 && (top1.score - curr.score) > dominantMargin) break;
|
|
292
|
+
// Relative to top1
|
|
293
|
+
if ((curr.score / Math.max(1e-6, top1.score)) < relativeThreshold) break;
|
|
294
|
+
// Cliff drop from previous
|
|
295
|
+
if (prev.score > 0 && (curr.score / prev.score) < cliffRatio) break;
|
|
296
|
+
} else {
|
|
297
|
+
if (curr.probability < minProb) break;
|
|
298
|
+
if (top1.probability >= 0.70 && (top1.probability - curr.probability) > 0.20) break;
|
|
299
|
+
if ((curr.probability / Math.max(1e-6, top1.probability)) < relativeThreshold) break;
|
|
300
|
+
if (prev.probability > 0 && (curr.probability / prev.probability) < cliffRatio) break;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
selected.push(curr);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (selected.length < minK) {
|
|
307
|
+
return candidates.slice(0, Math.min(candidates.length, minK));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return selected;
|
|
311
|
+
}
|
|
312
|
+
|
|
143
313
|
/**
|
|
144
314
|
* Main function following Hemanth module style:
|
|
145
315
|
* - One-shot: await toolPrune(query, tools, options)
|
|
@@ -153,6 +323,12 @@ export default function toolPrune(arg1, arg2, options) {
|
|
|
153
323
|
return new ToolPruner(arg1, arg2);
|
|
154
324
|
}
|
|
155
325
|
|
|
326
|
+
toolPrune.auto = function(query, tools, options) {
|
|
327
|
+
const pruner = new ToolPruner(tools, options);
|
|
328
|
+
return pruner.auto(query, options);
|
|
329
|
+
};
|
|
330
|
+
|
|
156
331
|
toolPrune.ToolPruner = ToolPruner;
|
|
157
332
|
toolPrune.normalizeTools = normalizeTools;
|
|
333
|
+
toolPrune.autoSelectCandidates = autoSelectCandidates;
|
|
158
334
|
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TurboQuant Vector Search Engine
|
|
3
|
+
* Data-oblivious quantization via Fast Walsh-Hadamard Transform + PolarQuant + QJL residual.
|
|
4
|
+
* Zero external dependencies. Uses turboquant-search (WASM) if installed.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Fast Walsh-Hadamard Transform in O(d log d)
|
|
8
|
+
function fwht(vec) {
|
|
9
|
+
const d = vec.length;
|
|
10
|
+
let h = 1;
|
|
11
|
+
while (h < d) {
|
|
12
|
+
for (let i = 0; i < d; i += 2 * h) {
|
|
13
|
+
for (let j = i; j < i + h; j++) {
|
|
14
|
+
const x = vec[j];
|
|
15
|
+
const y = vec[j + h];
|
|
16
|
+
vec[j] = x + y;
|
|
17
|
+
vec[j + h] = x - y;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
h *= 2;
|
|
21
|
+
}
|
|
22
|
+
const scale = 1 / Math.sqrt(d);
|
|
23
|
+
for (let i = 0; i < d; i++) vec[i] *= scale;
|
|
24
|
+
return vec;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function lcg(seed = 42) {
|
|
28
|
+
let s = seed;
|
|
29
|
+
return () => {
|
|
30
|
+
s = (s * 1664525 + 1013904223) % 4294967296;
|
|
31
|
+
return s / 4294967296;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class TurboQuantEngine {
|
|
36
|
+
constructor(options = {}) {
|
|
37
|
+
this.dim = options.dim || 256;
|
|
38
|
+
this.qjlDim = options.qjlDim || 64;
|
|
39
|
+
const rng = lcg(options.seed || 1337);
|
|
40
|
+
|
|
41
|
+
// Diagonal Rademacher matrix (+1 or -1)
|
|
42
|
+
this.rademacher = new Float64Array(this.dim);
|
|
43
|
+
for (let i = 0; i < this.dim; i++) {
|
|
44
|
+
this.rademacher[i] = rng() < 0.5 ? -1 : 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// QJL random projection matrix
|
|
48
|
+
this.qjlMatrix = new Array(this.qjlDim);
|
|
49
|
+
for (let i = 0; i < this.qjlDim; i++) {
|
|
50
|
+
const row = new Float64Array(this.dim);
|
|
51
|
+
for (let j = 0; j < this.dim; j++) {
|
|
52
|
+
row[j] = (rng() < 0.5 ? -1 : 1) / Math.sqrt(this.qjlDim);
|
|
53
|
+
}
|
|
54
|
+
this.qjlMatrix[i] = row;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.items = [];
|
|
58
|
+
this.index = [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
embedText(text) {
|
|
62
|
+
const vec = new Float64Array(this.dim);
|
|
63
|
+
const clean = text.toLowerCase();
|
|
64
|
+
const tokens = clean.replace(/[^a-z0-9_]/g, ' ').split(/\s+/).filter(t => t.length > 1);
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
67
|
+
const t = tokens[i];
|
|
68
|
+
this._hashFeature(t, 2.0, vec);
|
|
69
|
+
if (i < tokens.length - 1) {
|
|
70
|
+
this._hashFeature(`${t}_${tokens[i + 1]}`, 1.5, vec);
|
|
71
|
+
}
|
|
72
|
+
if (t.length >= 3) {
|
|
73
|
+
for (let j = 0; j <= t.length - 3; j++) {
|
|
74
|
+
this._hashFeature(t.slice(j, j + 3), 0.5, vec);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let norm = 0;
|
|
80
|
+
for (let i = 0; i < this.dim; i++) norm += vec[i] * vec[i];
|
|
81
|
+
if (norm > 0) {
|
|
82
|
+
norm = Math.sqrt(norm);
|
|
83
|
+
for (let i = 0; i < this.dim; i++) vec[i] /= norm;
|
|
84
|
+
}
|
|
85
|
+
return vec;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_hashFeature(feat, weight, vec) {
|
|
89
|
+
let h = 0x811c9dc5;
|
|
90
|
+
for (let i = 0; i < feat.length; i++) {
|
|
91
|
+
h ^= feat.charCodeAt(i);
|
|
92
|
+
h = Math.imul(h, 0x01000193);
|
|
93
|
+
}
|
|
94
|
+
const idx = Math.abs(h) % this.dim;
|
|
95
|
+
const sign = (h & 1) ? 1 : -1;
|
|
96
|
+
vec[idx] += sign * weight;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
quantize(vec) {
|
|
100
|
+
const rotated = new Float64Array(this.dim);
|
|
101
|
+
for (let i = 0; i < this.dim; i++) {
|
|
102
|
+
rotated[i] = vec[i] * this.rademacher[i];
|
|
103
|
+
}
|
|
104
|
+
fwht(rotated);
|
|
105
|
+
|
|
106
|
+
const std = 1.0 / Math.sqrt(this.dim);
|
|
107
|
+
const qLevels = new Float64Array([-1.18 * std, -0.38 * std, 0.38 * std, 1.18 * std]);
|
|
108
|
+
const dequant = new Float64Array(this.dim);
|
|
109
|
+
const residual = new Float64Array(this.dim);
|
|
110
|
+
|
|
111
|
+
for (let i = 0; i < this.dim; i++) {
|
|
112
|
+
const val = rotated[i];
|
|
113
|
+
let code = 0;
|
|
114
|
+
if (val < -0.67 * std) code = 0;
|
|
115
|
+
else if (val < 0) code = 1;
|
|
116
|
+
else if (val < 0.67 * std) code = 2;
|
|
117
|
+
else code = 3;
|
|
118
|
+
|
|
119
|
+
dequant[i] = qLevels[code];
|
|
120
|
+
residual[i] = val - dequant[i];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let resNorm = 0;
|
|
124
|
+
for (let i = 0; i < this.dim; i++) resNorm += residual[i] * residual[i];
|
|
125
|
+
resNorm = Math.sqrt(resNorm);
|
|
126
|
+
|
|
127
|
+
const qjlSigns = new Int8Array(this.qjlDim);
|
|
128
|
+
for (let i = 0; i < this.qjlDim; i++) {
|
|
129
|
+
let dot = 0;
|
|
130
|
+
const row = this.qjlMatrix[i];
|
|
131
|
+
for (let j = 0; j < this.dim; j++) dot += row[j] * residual[j];
|
|
132
|
+
qjlSigns[i] = dot >= 0 ? 1 : -1;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { dequant, resNorm, qjlSigns };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fit(toolsMap) {
|
|
139
|
+
this.items = Object.keys(toolsMap);
|
|
140
|
+
this.index = this.items.map(name => {
|
|
141
|
+
const tool = toolsMap[name];
|
|
142
|
+
const text = typeof tool === 'string'
|
|
143
|
+
? `${name} ${tool}`
|
|
144
|
+
: `${name} ${tool.description || ''} ${tool.criteria || ''} ${Object.keys(tool.parameters?.properties || {}).join(' ')}`;
|
|
145
|
+
const emb = this.embedText(text);
|
|
146
|
+
return {
|
|
147
|
+
name,
|
|
148
|
+
...this.quantize(emb)
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
search(query, topK = 3) {
|
|
154
|
+
const qEmb = this.embedText(query);
|
|
155
|
+
const qRotated = new Float64Array(this.dim);
|
|
156
|
+
for (let i = 0; i < this.dim; i++) {
|
|
157
|
+
qRotated[i] = qEmb[i] * this.rademacher[i];
|
|
158
|
+
}
|
|
159
|
+
fwht(qRotated);
|
|
160
|
+
|
|
161
|
+
const qQjl = new Float64Array(this.qjlDim);
|
|
162
|
+
for (let i = 0; i < this.qjlDim; i++) {
|
|
163
|
+
let dot = 0;
|
|
164
|
+
const row = this.qjlMatrix[i];
|
|
165
|
+
for (let j = 0; j < this.dim; j++) dot += row[j] * qRotated[j];
|
|
166
|
+
qQjl[i] = dot;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const scores = this.index.map(item => {
|
|
170
|
+
let dotBase = 0;
|
|
171
|
+
for (let i = 0; i < this.dim; i++) dotBase += qRotated[i] * item.dequant[i];
|
|
172
|
+
|
|
173
|
+
let qjlCorrection = 0;
|
|
174
|
+
for (let i = 0; i < this.qjlDim; i++) qjlCorrection += qQjl[i] * item.qjlSigns[i];
|
|
175
|
+
|
|
176
|
+
const residualEst = Math.sqrt(Math.PI / 2) * (item.resNorm / Math.sqrt(this.qjlDim)) * qjlCorrection;
|
|
177
|
+
const rawScore = dotBase + residualEst;
|
|
178
|
+
// Map to calibrated [0, 1] probability range
|
|
179
|
+
const prob = 1 / (1 + Math.exp(-Math.max(-10, Math.min(10, rawScore * 5))));
|
|
180
|
+
return { name: item.name, probability: prob, score: rawScore };
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
scores.sort((a, b) => b.score - a.score);
|
|
184
|
+
return scores.slice(0, topK);
|
|
185
|
+
}
|
|
186
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tool-prune",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Fast, calibrated tool selection and schema pruning for AI agents using TypeSafe System One.",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Fast, calibrated tool selection and schema pruning for AI agents using TypeSafe System One and TurboQuant vector search.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"types": "index.d.ts",
|
|
@@ -20,11 +20,13 @@
|
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
22
|
"test": "node --test test/index.test.js",
|
|
23
|
-
"demo": "node examples/quickstart.js"
|
|
23
|
+
"demo": "node examples/quickstart.js",
|
|
24
|
+
"playground": "node ../serve-playground.mjs"
|
|
24
25
|
},
|
|
25
26
|
"keywords": [
|
|
26
27
|
"tool-prune",
|
|
27
28
|
"tool-selection",
|
|
29
|
+
"turboquant",
|
|
28
30
|
"mcp",
|
|
29
31
|
"agent",
|
|
30
32
|
"typesafe",
|
|
@@ -38,6 +40,9 @@
|
|
|
38
40
|
"engines": {
|
|
39
41
|
"node": ">=18.0.0"
|
|
40
42
|
},
|
|
43
|
+
"optionalDependencies": {
|
|
44
|
+
"turboquant-search": "^0.1.1"
|
|
45
|
+
},
|
|
41
46
|
"repository": {
|
|
42
47
|
"type": "git",
|
|
43
48
|
"url": "https://github.com/hemanth/tool-prune"
|