tool-prune 0.1.0 → 0.2.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 +23 -8
- package/index.d.ts +3 -0
- package/lib/router.js +91 -4
- package/lib/turboquant.js +186 -0
- package/package.json +6 -2
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,19 @@ 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
|
-
const topTools = await router.filter(userPrompt, { k:
|
|
38
|
+
const topTools = await router.filter(userPrompt, { k: 5 });
|
|
32
39
|
|
|
33
40
|
const response = await llm.chat({
|
|
34
41
|
tools: topTools,
|
|
@@ -36,7 +43,7 @@ const response = await llm.chat({
|
|
|
36
43
|
});
|
|
37
44
|
```
|
|
38
45
|
|
|
39
|
-
|
|
46
|
+
Cuts prompt tokens by up to 92% and eliminates context confusion without losing tools.
|
|
40
47
|
|
|
41
48
|
## Fast-path direct dispatch
|
|
42
49
|
|
|
@@ -50,14 +57,22 @@ const result = await router.dispatch('read ./package.json', {
|
|
|
50
57
|
|
|
51
58
|
Runs deterministic handlers in under 160ms with zero token cost.
|
|
52
59
|
|
|
60
|
+
## Dual engine
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
const localMatch = await prune(query, tools, { engine: 'turboquant' });
|
|
64
|
+
const cloudMatch = await prune(query, tools, { engine: 'typesafe', apiKey: '...' });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- **turboquant**: 100% offline, zero network, zero dependencies. Uses `turboquant-search` (WASM SIMD) if installed, with built-in FWHT fallback.
|
|
68
|
+
- **typesafe**: Cloud System One reasoning (Jev). 100% Top-1 accuracy on subtle distractors with calibrated probabilities.
|
|
69
|
+
|
|
53
70
|
## Demo
|
|
54
71
|
|
|
55
72
|
```bash
|
|
56
73
|
npm run demo
|
|
57
74
|
```
|
|
58
75
|
|
|
59
|
-
Runs live quickstart routing with your `TYPESAFE_API_KEY`.
|
|
60
|
-
|
|
61
76
|
## License
|
|
62
77
|
|
|
63
78
|
MIT © [Hemanth.HM](https://h3manth.com)
|
package/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface ToolPruneOptions {
|
|
|
13
13
|
apiKey?: string;
|
|
14
14
|
endpoint?: string;
|
|
15
15
|
model?: string;
|
|
16
|
+
engine?: 'typesafe' | 'turboquant';
|
|
16
17
|
threshold?: number;
|
|
17
18
|
topK?: number;
|
|
18
19
|
}
|
|
@@ -20,6 +21,7 @@ export interface ToolPruneOptions {
|
|
|
20
21
|
export interface CandidateTool {
|
|
21
22
|
name: string;
|
|
22
23
|
probability: number;
|
|
24
|
+
score?: number;
|
|
23
25
|
tool?: any;
|
|
24
26
|
}
|
|
25
27
|
|
|
@@ -30,6 +32,7 @@ export interface SelectionResult {
|
|
|
30
32
|
topK: CandidateTool[];
|
|
31
33
|
requiresGeneration: number;
|
|
32
34
|
latency: number;
|
|
35
|
+
engine?: string;
|
|
33
36
|
usage?: {
|
|
34
37
|
input_tokens: number;
|
|
35
38
|
output_tokens: number;
|
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
|
*/
|
|
@@ -40,14 +42,35 @@ export class ToolPruner {
|
|
|
40
42
|
this.model = options.model || 'jev-latest';
|
|
41
43
|
this.threshold = options.threshold ?? 0.85;
|
|
42
44
|
this.defaultTopK = options.topK ?? 3;
|
|
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)
|
|
@@ -107,11 +130,76 @@ export class ToolPruner {
|
|
|
107
130
|
topK,
|
|
108
131
|
requiresGeneration,
|
|
109
132
|
latency,
|
|
133
|
+
engine: 'typesafe',
|
|
110
134
|
usage: data.usage,
|
|
111
135
|
raw: data
|
|
112
136
|
};
|
|
113
137
|
}
|
|
114
138
|
|
|
139
|
+
async _selectTurboQuant(query, options = {}) {
|
|
140
|
+
const qStr = typeof query === 'string' ? query : (query.intent || query.query || JSON.stringify(query));
|
|
141
|
+
const start = performance.now();
|
|
142
|
+
|
|
143
|
+
// Check if turboquant-search (WASM) is available
|
|
144
|
+
if (!this._tqEngine) {
|
|
145
|
+
try {
|
|
146
|
+
const { TurboSearch } = await import('turboquant-search');
|
|
147
|
+
const data = Array.from(this.registry.values()).map(t => ({
|
|
148
|
+
name: t.name,
|
|
149
|
+
description: t.description || '',
|
|
150
|
+
criteria: t.criteria || ''
|
|
151
|
+
}));
|
|
152
|
+
this._wasmEngine = await TurboSearch.from(data, {
|
|
153
|
+
fields: ['name', 'description', 'criteria'],
|
|
154
|
+
dim: 384,
|
|
155
|
+
bits: 3
|
|
156
|
+
});
|
|
157
|
+
} catch {
|
|
158
|
+
// Fallback to built-in zero-dependency TurboQuant engine
|
|
159
|
+
this._tqEngine = new TurboQuantEngine();
|
|
160
|
+
const toolsMap = {};
|
|
161
|
+
for (const [name, tool] of this.registry.entries()) {
|
|
162
|
+
toolsMap[name] = tool;
|
|
163
|
+
}
|
|
164
|
+
this._tqEngine.fit(toolsMap);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const k = options.topK || this.defaultTopK;
|
|
169
|
+
let topK = [];
|
|
170
|
+
|
|
171
|
+
if (this._wasmEngine) {
|
|
172
|
+
const results = await this._wasmEngine.search(qStr, { topK: k });
|
|
173
|
+
topK = results.map(r => ({
|
|
174
|
+
name: r.data?.name || '',
|
|
175
|
+
probability: r.score || 0,
|
|
176
|
+
score: r.score || 0,
|
|
177
|
+
tool: this.registry.get(r.data?.name)
|
|
178
|
+
}));
|
|
179
|
+
} else {
|
|
180
|
+
const rawResults = this._tqEngine.search(qStr, k);
|
|
181
|
+
topK = rawResults.map(r => ({
|
|
182
|
+
name: r.name,
|
|
183
|
+
probability: r.probability,
|
|
184
|
+
score: r.score,
|
|
185
|
+
tool: this.registry.get(r.name)
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const latency = performance.now() - start;
|
|
190
|
+
const top1 = topK[0];
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
tool: top1?.name || '',
|
|
194
|
+
confidence: top1?.probability || 0,
|
|
195
|
+
probability: top1?.probability || 0,
|
|
196
|
+
topK,
|
|
197
|
+
requiresGeneration: 0,
|
|
198
|
+
latency,
|
|
199
|
+
engine: 'turboquant'
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
115
203
|
/**
|
|
116
204
|
* Filter tool collection down to top-K candidates to prune LLM prompt bloat.
|
|
117
205
|
*/
|
|
@@ -155,4 +243,3 @@ export default function toolPrune(arg1, arg2, options) {
|
|
|
155
243
|
|
|
156
244
|
toolPrune.ToolPruner = ToolPruner;
|
|
157
245
|
toolPrune.normalizeTools = normalizeTools;
|
|
158
|
-
|
|
@@ -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.2.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",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"keywords": [
|
|
26
26
|
"tool-prune",
|
|
27
27
|
"tool-selection",
|
|
28
|
+
"turboquant",
|
|
28
29
|
"mcp",
|
|
29
30
|
"agent",
|
|
30
31
|
"typesafe",
|
|
@@ -38,6 +39,9 @@
|
|
|
38
39
|
"engines": {
|
|
39
40
|
"node": ">=18.0.0"
|
|
40
41
|
},
|
|
42
|
+
"optionalDependencies": {
|
|
43
|
+
"turboquant-search": "^0.1.1"
|
|
44
|
+
},
|
|
41
45
|
"repository": {
|
|
42
46
|
"type": "git",
|
|
43
47
|
"url": "https://github.com/hemanth/tool-prune"
|