tool-prune 0.2.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 +3 -1
- package/index.d.ts +18 -2
- package/lib/router.js +103 -14
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -35,7 +35,9 @@ console.log(match.engine); // 'turboquant'
|
|
|
35
35
|
|
|
36
36
|
```js
|
|
37
37
|
const router = prune(tools);
|
|
38
|
-
|
|
38
|
+
|
|
39
|
+
// Auto-selects candidate schemas dynamically (or pass { k: 5 }):
|
|
40
|
+
const topTools = await router.filter(userPrompt);
|
|
39
41
|
|
|
40
42
|
const response = await llm.chat({
|
|
41
43
|
tools: topTools,
|
package/index.d.ts
CHANGED
|
@@ -15,7 +15,17 @@ export interface ToolPruneOptions {
|
|
|
15
15
|
model?: string;
|
|
16
16
|
engine?: 'typesafe' | 'turboquant';
|
|
17
17
|
threshold?: number;
|
|
18
|
-
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;
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
export interface CandidateTool {
|
|
@@ -30,6 +40,8 @@ export interface SelectionResult {
|
|
|
30
40
|
confidence: number;
|
|
31
41
|
probability: number;
|
|
32
42
|
topK: CandidateTool[];
|
|
43
|
+
autoSelected: CandidateTool[];
|
|
44
|
+
autoTools: any[];
|
|
33
45
|
requiresGeneration: number;
|
|
34
46
|
latency: number;
|
|
35
47
|
engine?: string;
|
|
@@ -40,10 +52,13 @@ export interface SelectionResult {
|
|
|
40
52
|
raw?: any;
|
|
41
53
|
}
|
|
42
54
|
|
|
55
|
+
export function autoSelectCandidates(candidates: CandidateTool[], options?: Partial<ToolPruneOptions>): CandidateTool[];
|
|
56
|
+
|
|
43
57
|
export class ToolPruner {
|
|
44
58
|
constructor(tools: ToolInput, options?: ToolPruneOptions);
|
|
45
59
|
select(query: string | Record<string, any>, options?: Partial<ToolPruneOptions>): Promise<SelectionResult>;
|
|
46
|
-
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[]>;
|
|
47
62
|
dispatch<T = any>(
|
|
48
63
|
query: string | Record<string, any>,
|
|
49
64
|
handlers: Record<string, (query: any, selection: SelectionResult) => Promise<T> | T>,
|
|
@@ -54,3 +69,4 @@ export class ToolPruner {
|
|
|
54
69
|
export default function toolPrune(query: string, tools: ToolInput, options?: ToolPruneOptions): Promise<SelectionResult>;
|
|
55
70
|
export default function toolPrune(tools: ToolInput, options?: ToolPruneOptions): ToolPruner;
|
|
56
71
|
|
|
72
|
+
|
package/lib/router.js
CHANGED
|
@@ -41,7 +41,7 @@ export class ToolPruner {
|
|
|
41
41
|
this.endpoint = options.endpoint || 'https://api.typesafe.ai/v1/systemone';
|
|
42
42
|
this.model = options.model || 'jev-latest';
|
|
43
43
|
this.threshold = options.threshold ?? 0.85;
|
|
44
|
-
this.defaultTopK = options.topK ??
|
|
44
|
+
this.defaultTopK = options.topK ?? 'auto';
|
|
45
45
|
this.requestedEngine = options.engine;
|
|
46
46
|
this._tqEngine = null;
|
|
47
47
|
this._wasmEngine = null;
|
|
@@ -111,14 +111,19 @@ export class ToolPruner {
|
|
|
111
111
|
const toolAnswer = data.answers?.tool;
|
|
112
112
|
const probs = toolAnswer?.probabilities || {};
|
|
113
113
|
const sorted = Object.entries(probs).sort((a, b) => b[1] - a[1]);
|
|
114
|
-
const
|
|
115
|
-
const topK = sorted.slice(0, topKCount).map(([name, p]) => ({
|
|
114
|
+
const allCandidates = sorted.map(([name, p]) => ({
|
|
116
115
|
name,
|
|
117
116
|
probability: p,
|
|
118
117
|
tool: this.registry.get(name)
|
|
119
118
|
}));
|
|
120
119
|
|
|
121
|
-
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 || '';
|
|
122
127
|
const probability = probs[selectedName] ?? 0;
|
|
123
128
|
const confidence = toolAnswer?.confidence ?? probability;
|
|
124
129
|
const requiresGeneration = data.answers?.requires_generation?.probability ?? 0;
|
|
@@ -128,6 +133,8 @@ export class ToolPruner {
|
|
|
128
133
|
confidence,
|
|
129
134
|
probability,
|
|
130
135
|
topK,
|
|
136
|
+
autoSelected,
|
|
137
|
+
autoTools,
|
|
131
138
|
requiresGeneration,
|
|
132
139
|
latency,
|
|
133
140
|
engine: 'typesafe',
|
|
@@ -165,20 +172,21 @@ export class ToolPruner {
|
|
|
165
172
|
}
|
|
166
173
|
}
|
|
167
174
|
|
|
168
|
-
const k = options.topK
|
|
169
|
-
|
|
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 = [];
|
|
170
178
|
|
|
171
179
|
if (this._wasmEngine) {
|
|
172
|
-
const results = await this._wasmEngine.search(qStr, { topK:
|
|
173
|
-
|
|
180
|
+
const results = await this._wasmEngine.search(qStr, { topK: poolSize });
|
|
181
|
+
allCandidates = results.map(r => ({
|
|
174
182
|
name: r.data?.name || '',
|
|
175
183
|
probability: r.score || 0,
|
|
176
184
|
score: r.score || 0,
|
|
177
185
|
tool: this.registry.get(r.data?.name)
|
|
178
186
|
}));
|
|
179
187
|
} else {
|
|
180
|
-
const rawResults = this._tqEngine.search(qStr,
|
|
181
|
-
|
|
188
|
+
const rawResults = this._tqEngine.search(qStr, poolSize);
|
|
189
|
+
allCandidates = rawResults.map(r => ({
|
|
182
190
|
name: r.name,
|
|
183
191
|
probability: r.probability,
|
|
184
192
|
score: r.score,
|
|
@@ -187,13 +195,18 @@ export class ToolPruner {
|
|
|
187
195
|
}
|
|
188
196
|
|
|
189
197
|
const latency = performance.now() - start;
|
|
190
|
-
const top1 =
|
|
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));
|
|
191
202
|
|
|
192
203
|
return {
|
|
193
204
|
tool: top1?.name || '',
|
|
194
205
|
confidence: top1?.probability || 0,
|
|
195
206
|
probability: top1?.probability || 0,
|
|
196
207
|
topK,
|
|
208
|
+
autoSelected,
|
|
209
|
+
autoTools,
|
|
197
210
|
requiresGeneration: 0,
|
|
198
211
|
latency,
|
|
199
212
|
engine: 'turboquant'
|
|
@@ -201,12 +214,26 @@ export class ToolPruner {
|
|
|
201
214
|
}
|
|
202
215
|
|
|
203
216
|
/**
|
|
204
|
-
* 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.
|
|
205
220
|
*/
|
|
206
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 = {}) {
|
|
207
235
|
const res = await this.select(query, options);
|
|
208
|
-
|
|
209
|
-
return res.topK.slice(0, k).map(item => item.tool || item.name);
|
|
236
|
+
return res.autoTools;
|
|
210
237
|
}
|
|
211
238
|
|
|
212
239
|
/**
|
|
@@ -228,6 +255,61 @@ export class ToolPruner {
|
|
|
228
255
|
}
|
|
229
256
|
}
|
|
230
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
|
+
|
|
231
313
|
/**
|
|
232
314
|
* Main function following Hemanth module style:
|
|
233
315
|
* - One-shot: await toolPrune(query, tools, options)
|
|
@@ -241,5 +323,12 @@ export default function toolPrune(arg1, arg2, options) {
|
|
|
241
323
|
return new ToolPruner(arg1, arg2);
|
|
242
324
|
}
|
|
243
325
|
|
|
326
|
+
toolPrune.auto = function(query, tools, options) {
|
|
327
|
+
const pruner = new ToolPruner(tools, options);
|
|
328
|
+
return pruner.auto(query, options);
|
|
329
|
+
};
|
|
330
|
+
|
|
244
331
|
toolPrune.ToolPruner = ToolPruner;
|
|
245
332
|
toolPrune.normalizeTools = normalizeTools;
|
|
333
|
+
toolPrune.autoSelectCandidates = autoSelectCandidates;
|
|
334
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tool-prune",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
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",
|
|
@@ -20,7 +20,8 @@
|
|
|
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",
|