sis-tools 0.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/dist/index.cjs +1531 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +869 -0
- package/dist/index.d.ts +869 -0
- package/dist/index.js +1476 -0
- package/dist/index.js.map +1 -0
- package/package.json +86 -0
- package/src/embeddings/base.ts +47 -0
- package/src/embeddings/cohere.ts +79 -0
- package/src/embeddings/google.ts +67 -0
- package/src/embeddings/index.ts +43 -0
- package/src/embeddings/openai.ts +87 -0
- package/src/formatters.ts +249 -0
- package/src/hooks.ts +341 -0
- package/src/index.ts +104 -0
- package/src/optional-peer-deps.d.ts +17 -0
- package/src/scoring.ts +198 -0
- package/src/sis.ts +572 -0
- package/src/store.ts +134 -0
- package/src/types.ts +136 -0
- package/src/validators.ts +484 -0
package/src/sis.ts
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core SIS implementation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
Tool,
|
|
7
|
+
ToolHandler,
|
|
8
|
+
ToolParameters,
|
|
9
|
+
ToolExample,
|
|
10
|
+
ToolMetadata,
|
|
11
|
+
ResolvedTool,
|
|
12
|
+
OpenAIToolSchema,
|
|
13
|
+
AnthropicToolSchema,
|
|
14
|
+
} from "./types";
|
|
15
|
+
import { toSchema, toOpenAISchema, toAnthropicSchema } from "./types";
|
|
16
|
+
import type { EmbeddingProvider, ProviderName, ProviderOptions } from "./embeddings";
|
|
17
|
+
import { getProvider, buildToolText } from "./embeddings";
|
|
18
|
+
import { VectorStore } from "./store";
|
|
19
|
+
|
|
20
|
+
// Import customization systems
|
|
21
|
+
import type { SimilarityFunction, ScoringFunction } from "./scoring";
|
|
22
|
+
import { DEFAULT_SIMILARITY, DEFAULT_SCORING } from "./scoring";
|
|
23
|
+
import type { ToolFormatter } from "./formatters";
|
|
24
|
+
import { getFormatter, hasFormatter, formatTools } from "./formatters";
|
|
25
|
+
import type { Hook } from "./hooks";
|
|
26
|
+
import { HookRegistry, HookType, createContext } from "./hooks";
|
|
27
|
+
import type { ValidatorRegistry } from "./validators";
|
|
28
|
+
import { ValidationError } from "./validators";
|
|
29
|
+
|
|
30
|
+
export interface SISOptions {
|
|
31
|
+
embeddingProvider?: ProviderName | EmbeddingProvider;
|
|
32
|
+
defaultTopK?: number;
|
|
33
|
+
defaultThreshold?: number;
|
|
34
|
+
providerOptions?: ProviderOptions;
|
|
35
|
+
remoteUrl?: string;
|
|
36
|
+
projectId?: string;
|
|
37
|
+
// New customization options
|
|
38
|
+
similarity?: SimilarityFunction;
|
|
39
|
+
scoring?: ScoringFunction;
|
|
40
|
+
validators?: ValidatorRegistry;
|
|
41
|
+
validateOnRegister?: boolean;
|
|
42
|
+
validateOnExecute?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RegisterOptions {
|
|
46
|
+
name: string;
|
|
47
|
+
description: string;
|
|
48
|
+
parameters?: ToolParameters;
|
|
49
|
+
handler?: ToolHandler;
|
|
50
|
+
semanticHints?: string[];
|
|
51
|
+
examples?: ToolExample[];
|
|
52
|
+
metadata?: ToolMetadata;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface StoreOptions extends RegisterOptions {
|
|
56
|
+
embedding: number[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type ResolveFormat = "raw" | "openai" | "anthropic" | string;
|
|
60
|
+
|
|
61
|
+
export interface ResolveOptions {
|
|
62
|
+
topK?: number;
|
|
63
|
+
threshold?: number;
|
|
64
|
+
format?: ResolveFormat | ToolFormatter;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Semantic Integration System - Intelligent tool resolution
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* const sis = new SIS({ embeddingProvider: 'openai' });
|
|
72
|
+
*
|
|
73
|
+
* sis.register({
|
|
74
|
+
* name: 'web_search',
|
|
75
|
+
* description: 'Search the web for information',
|
|
76
|
+
* parameters: { query: { type: 'string' } },
|
|
77
|
+
* handler: async ({ query }) => searchApi(query)
|
|
78
|
+
* });
|
|
79
|
+
*
|
|
80
|
+
* await sis.initialize();
|
|
81
|
+
* const tools = await sis.resolve("what's the weather?");
|
|
82
|
+
*
|
|
83
|
+
* // With custom formatter
|
|
84
|
+
* const geminiTools = await sis.resolve("query", { format: "gemini" });
|
|
85
|
+
*/
|
|
86
|
+
export class SIS {
|
|
87
|
+
private _embeddings: EmbeddingProvider;
|
|
88
|
+
private _vectorStore: VectorStore;
|
|
89
|
+
private _pendingTools: Tool[];
|
|
90
|
+
private _defaultTopK: number;
|
|
91
|
+
private _defaultThreshold: number;
|
|
92
|
+
private _initialized: boolean;
|
|
93
|
+
private _remoteUrl?: string;
|
|
94
|
+
private _projectId?: string;
|
|
95
|
+
|
|
96
|
+
// Customization systems
|
|
97
|
+
private _similarity: SimilarityFunction;
|
|
98
|
+
private _scoring: ScoringFunction;
|
|
99
|
+
private _hooks: HookRegistry;
|
|
100
|
+
private _validators?: ValidatorRegistry;
|
|
101
|
+
private _validateOnRegister: boolean;
|
|
102
|
+
private _validateOnExecute: boolean;
|
|
103
|
+
|
|
104
|
+
constructor(options: SISOptions = {}) {
|
|
105
|
+
const {
|
|
106
|
+
embeddingProvider = "openai",
|
|
107
|
+
defaultTopK = 5,
|
|
108
|
+
defaultThreshold = 0.3,
|
|
109
|
+
providerOptions = {},
|
|
110
|
+
similarity,
|
|
111
|
+
scoring,
|
|
112
|
+
validators,
|
|
113
|
+
validateOnRegister = false,
|
|
114
|
+
validateOnExecute = false,
|
|
115
|
+
} = options;
|
|
116
|
+
|
|
117
|
+
if (typeof embeddingProvider === "string") {
|
|
118
|
+
this._embeddings = getProvider(embeddingProvider, providerOptions);
|
|
119
|
+
} else {
|
|
120
|
+
this._embeddings = embeddingProvider;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this._vectorStore = new VectorStore();
|
|
124
|
+
this._pendingTools = [];
|
|
125
|
+
this._defaultTopK = defaultTopK;
|
|
126
|
+
this._defaultThreshold = defaultThreshold;
|
|
127
|
+
this._initialized = false;
|
|
128
|
+
this._remoteUrl = options.remoteUrl;
|
|
129
|
+
this._projectId = options.projectId;
|
|
130
|
+
|
|
131
|
+
// Initialize customization systems
|
|
132
|
+
this._similarity = similarity ?? DEFAULT_SIMILARITY;
|
|
133
|
+
this._scoring = scoring ?? DEFAULT_SCORING;
|
|
134
|
+
this._hooks = new HookRegistry();
|
|
135
|
+
this._validators = validators;
|
|
136
|
+
this._validateOnRegister = validateOnRegister;
|
|
137
|
+
this._validateOnExecute = validateOnExecute;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Properties for accessing customization systems
|
|
141
|
+
|
|
142
|
+
/** Get the hook registry for registering middleware */
|
|
143
|
+
get hooks(): HookRegistry {
|
|
144
|
+
return this._hooks;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Get the validator registry */
|
|
148
|
+
get validators(): ValidatorRegistry | undefined {
|
|
149
|
+
return this._validators;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Get the current similarity function */
|
|
153
|
+
get similarity(): SimilarityFunction {
|
|
154
|
+
return this._similarity;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Set a new similarity function */
|
|
158
|
+
set similarity(fn: SimilarityFunction) {
|
|
159
|
+
this._similarity = fn;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Get the current scoring function */
|
|
163
|
+
get scoring(): ScoringFunction {
|
|
164
|
+
return this._scoring;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Set a new scoring function */
|
|
168
|
+
set scoring(fn: ScoringFunction) {
|
|
169
|
+
this._scoring = fn;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Register a hook */
|
|
173
|
+
registerHook(hook: Hook): void {
|
|
174
|
+
this._hooks.register(hook);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Unregister a hook */
|
|
178
|
+
unregisterHook(hook: Hook): boolean {
|
|
179
|
+
return this._hooks.unregister(hook);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Register a tool programmatically
|
|
184
|
+
*/
|
|
185
|
+
register(options: RegisterOptions): Tool {
|
|
186
|
+
const tool: Tool = {
|
|
187
|
+
name: options.name,
|
|
188
|
+
description: options.description,
|
|
189
|
+
parameters: options.parameters ?? {},
|
|
190
|
+
semanticHints: options.semanticHints ?? [],
|
|
191
|
+
examples: options.examples ?? [],
|
|
192
|
+
handler: options.handler,
|
|
193
|
+
metadata: options.metadata ?? {},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// Validate if enabled
|
|
197
|
+
if (this._validateOnRegister && this._validators) {
|
|
198
|
+
const result = this._validators.validateTool(tool);
|
|
199
|
+
if (!result.valid) {
|
|
200
|
+
throw new ValidationError(result);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
this._pendingTools.push(tool);
|
|
205
|
+
return tool;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Store (upsert) a tool with a precomputed embedding.
|
|
210
|
+
*
|
|
211
|
+
* This bypasses the embedding provider, allowing custom embedding workflows.
|
|
212
|
+
*/
|
|
213
|
+
store(options: StoreOptions): Tool {
|
|
214
|
+
const tool: Tool = {
|
|
215
|
+
name: options.name,
|
|
216
|
+
description: options.description,
|
|
217
|
+
parameters: options.parameters ?? {},
|
|
218
|
+
semanticHints: options.semanticHints ?? [],
|
|
219
|
+
examples: options.examples ?? [],
|
|
220
|
+
handler: options.handler,
|
|
221
|
+
metadata: options.metadata ?? {},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Validate tool schema if enabled
|
|
225
|
+
if (this._validateOnRegister && this._validators) {
|
|
226
|
+
const result = this._validators.validateTool(tool);
|
|
227
|
+
if (!result.valid) {
|
|
228
|
+
throw new ValidationError(result);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Validate embedding
|
|
233
|
+
if (this._validators) {
|
|
234
|
+
const embeddingResult = this._validators.validateEmbedding(options.embedding);
|
|
235
|
+
if (!embeddingResult.valid) {
|
|
236
|
+
throw new ValidationError(embeddingResult);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (options.embedding.length !== this._embeddings.dimensions) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`Embedding dimensions must match provider: ${options.embedding.length} !== ${this._embeddings.dimensions}`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Upsert into the local store
|
|
247
|
+
if (this._vectorStore.has(tool.name)) {
|
|
248
|
+
this._vectorStore.remove(tool.name);
|
|
249
|
+
}
|
|
250
|
+
this._vectorStore.add(tool, options.embedding);
|
|
251
|
+
return tool;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Initialize embeddings for all pending tools
|
|
256
|
+
*/
|
|
257
|
+
async initialize(): Promise<void> {
|
|
258
|
+
if (this._pendingTools.length === 0) {
|
|
259
|
+
this._initialized = true;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// PRE_EMBED hook
|
|
264
|
+
let ctx = createContext(HookType.PRE_EMBED, { tools: this._pendingTools });
|
|
265
|
+
ctx = await this._hooks.run(HookType.PRE_EMBED, ctx);
|
|
266
|
+
if (ctx.cancelled) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Build text representations for embedding
|
|
271
|
+
const texts = this._pendingTools.map((tool) =>
|
|
272
|
+
buildToolText(
|
|
273
|
+
tool.name,
|
|
274
|
+
tool.description,
|
|
275
|
+
tool.semanticHints,
|
|
276
|
+
tool.examples
|
|
277
|
+
)
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
// Batch embed all tools
|
|
281
|
+
const embeddings = await this._embeddings.embedBatch(texts);
|
|
282
|
+
|
|
283
|
+
// Add to store
|
|
284
|
+
this._vectorStore.addBatch(this._pendingTools, embeddings);
|
|
285
|
+
|
|
286
|
+
// POST_EMBED hook
|
|
287
|
+
ctx = createContext(HookType.POST_EMBED, {
|
|
288
|
+
tools: this._pendingTools,
|
|
289
|
+
embeddings,
|
|
290
|
+
});
|
|
291
|
+
await this._hooks.run(HookType.POST_EMBED, ctx);
|
|
292
|
+
|
|
293
|
+
this._pendingTools = [];
|
|
294
|
+
this._initialized = true;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Resolve tools for a query (raw format)
|
|
299
|
+
*/
|
|
300
|
+
async resolve(
|
|
301
|
+
query: string,
|
|
302
|
+
options?: { topK?: number; threshold?: number; format?: "raw" }
|
|
303
|
+
): Promise<ResolvedTool[]>;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Resolve tools for a query (OpenAI format)
|
|
307
|
+
*/
|
|
308
|
+
async resolve(
|
|
309
|
+
query: string,
|
|
310
|
+
options: { topK?: number; threshold?: number; format: "openai" }
|
|
311
|
+
): Promise<OpenAIToolSchema[]>;
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Resolve tools for a query (Anthropic format)
|
|
315
|
+
*/
|
|
316
|
+
async resolve(
|
|
317
|
+
query: string,
|
|
318
|
+
options: { topK?: number; threshold?: number; format: "anthropic" }
|
|
319
|
+
): Promise<AnthropicToolSchema[]>;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Resolve tools for a query (custom format)
|
|
323
|
+
*/
|
|
324
|
+
async resolve(
|
|
325
|
+
query: string,
|
|
326
|
+
options: { topK?: number; threshold?: number; format: string | ToolFormatter }
|
|
327
|
+
): Promise<Record<string, unknown>[]>;
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Resolve tools for a query
|
|
331
|
+
*/
|
|
332
|
+
async resolve(
|
|
333
|
+
query: string,
|
|
334
|
+
options: ResolveOptions = {}
|
|
335
|
+
): Promise<ResolvedTool[] | OpenAIToolSchema[] | AnthropicToolSchema[] | Record<string, unknown>[]> {
|
|
336
|
+
// Auto-initialize if needed
|
|
337
|
+
if (!this._initialized) {
|
|
338
|
+
await this.initialize();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const topK = options.topK ?? this._defaultTopK;
|
|
342
|
+
const threshold = options.threshold ?? this._defaultThreshold;
|
|
343
|
+
const format = options.format ?? "raw";
|
|
344
|
+
|
|
345
|
+
// PRE_RESOLVE hook
|
|
346
|
+
let ctx = createContext(HookType.PRE_RESOLVE, {
|
|
347
|
+
query,
|
|
348
|
+
topK,
|
|
349
|
+
threshold,
|
|
350
|
+
format,
|
|
351
|
+
});
|
|
352
|
+
ctx = await this._hooks.run(HookType.PRE_RESOLVE, ctx);
|
|
353
|
+
if (ctx.cancelled) {
|
|
354
|
+
if (ctx.data.cachedResults) {
|
|
355
|
+
return ctx.data.cachedResults as ResolvedTool[];
|
|
356
|
+
}
|
|
357
|
+
return [];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Allow hooks to modify parameters
|
|
361
|
+
const finalQuery = (ctx.data.query as string) ?? query;
|
|
362
|
+
const finalTopK = (ctx.data.topK as number) ?? topK;
|
|
363
|
+
const finalThreshold = (ctx.data.threshold as number) ?? threshold;
|
|
364
|
+
|
|
365
|
+
// Check for remote resolution
|
|
366
|
+
if (this._remoteUrl && this._projectId) {
|
|
367
|
+
const response = await fetch(
|
|
368
|
+
`${this._remoteUrl}/v1/projects/${this._projectId}/resolve`,
|
|
369
|
+
{
|
|
370
|
+
method: "POST",
|
|
371
|
+
headers: { "Content-Type": "application/json" },
|
|
372
|
+
body: JSON.stringify({ query: finalQuery, top_k: finalTopK, threshold: finalThreshold }),
|
|
373
|
+
}
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
if (!response.ok) {
|
|
377
|
+
throw new Error(`Remote resolution failed: ${response.statusText}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const data = (await response.json()) as { results: ResolvedTool[] };
|
|
381
|
+
const results = data.results;
|
|
382
|
+
|
|
383
|
+
return this.formatResults(results, format);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Embed the query
|
|
387
|
+
const queryEmbedding = await this._embeddings.embed(finalQuery);
|
|
388
|
+
|
|
389
|
+
// PRE_SEARCH hook
|
|
390
|
+
let searchCtx = createContext(HookType.PRE_SEARCH, {
|
|
391
|
+
query: finalQuery,
|
|
392
|
+
queryEmbedding,
|
|
393
|
+
topK: finalTopK,
|
|
394
|
+
threshold: finalThreshold,
|
|
395
|
+
});
|
|
396
|
+
searchCtx = await this._hooks.run(HookType.PRE_SEARCH, searchCtx);
|
|
397
|
+
|
|
398
|
+
// Search for matching tools with custom similarity/scoring
|
|
399
|
+
const matches = this._vectorStore.search(
|
|
400
|
+
queryEmbedding,
|
|
401
|
+
finalTopK,
|
|
402
|
+
finalThreshold,
|
|
403
|
+
this._similarity,
|
|
404
|
+
this._scoring
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
// POST_SEARCH hook
|
|
408
|
+
let postSearchCtx = createContext(HookType.POST_SEARCH, {
|
|
409
|
+
query: finalQuery,
|
|
410
|
+
matches,
|
|
411
|
+
});
|
|
412
|
+
postSearchCtx = await this._hooks.run(HookType.POST_SEARCH, postSearchCtx);
|
|
413
|
+
const finalMatches = (postSearchCtx.data.matches as typeof matches) ?? matches;
|
|
414
|
+
|
|
415
|
+
// Convert to resolved tools
|
|
416
|
+
const resolved: ResolvedTool[] = finalMatches.map((match) => ({
|
|
417
|
+
name: match.tool.name,
|
|
418
|
+
schema: toSchema(match.tool),
|
|
419
|
+
score: match.score,
|
|
420
|
+
handler: match.tool.handler,
|
|
421
|
+
}));
|
|
422
|
+
|
|
423
|
+
// Format output
|
|
424
|
+
const results = this.formatResults(resolved, format);
|
|
425
|
+
|
|
426
|
+
// POST_RESOLVE hook
|
|
427
|
+
let postCtx = createContext(HookType.POST_RESOLVE, {
|
|
428
|
+
query: finalQuery,
|
|
429
|
+
results,
|
|
430
|
+
resolved,
|
|
431
|
+
});
|
|
432
|
+
postCtx = await this._hooks.run(HookType.POST_RESOLVE, postCtx);
|
|
433
|
+
|
|
434
|
+
return (postCtx.data.results as typeof results) ?? results;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Format results based on format option
|
|
439
|
+
*/
|
|
440
|
+
private formatResults(
|
|
441
|
+
resolved: ResolvedTool[],
|
|
442
|
+
format: ResolveFormat | ToolFormatter
|
|
443
|
+
): ResolvedTool[] | OpenAIToolSchema[] | AnthropicToolSchema[] | Record<string, unknown>[] {
|
|
444
|
+
if (format === "raw") {
|
|
445
|
+
return resolved;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (typeof format === "object" && "format" in format) {
|
|
449
|
+
// ToolFormatter instance
|
|
450
|
+
return formatTools(resolved, format);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (typeof format === "string") {
|
|
454
|
+
// Check if it's a registered formatter
|
|
455
|
+
if (hasFormatter(format)) {
|
|
456
|
+
return formatTools(resolved, format);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Built-in formats
|
|
460
|
+
if (format === "openai") {
|
|
461
|
+
return resolved.map((r) => ({
|
|
462
|
+
type: "function" as const,
|
|
463
|
+
function: r.schema,
|
|
464
|
+
}));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (format === "anthropic") {
|
|
468
|
+
return resolved.map((r) => ({
|
|
469
|
+
name: r.schema.name,
|
|
470
|
+
description: r.schema.description,
|
|
471
|
+
input_schema: r.schema.parameters,
|
|
472
|
+
}));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
throw new Error(`Unknown format: ${format}`);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return resolved;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Resolve the single best matching tool
|
|
483
|
+
*/
|
|
484
|
+
async resolveOne(
|
|
485
|
+
query: string,
|
|
486
|
+
threshold?: number
|
|
487
|
+
): Promise<ResolvedTool | null> {
|
|
488
|
+
const results = await this.resolve(query, { topK: 1, threshold });
|
|
489
|
+
return results.length > 0 ? (results[0] as ResolvedTool) : null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Get a registered tool by name
|
|
494
|
+
*/
|
|
495
|
+
getTool(name: string): Tool | undefined {
|
|
496
|
+
return this._vectorStore.get(name);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* List all registered tool names
|
|
501
|
+
*/
|
|
502
|
+
listTools(): string[] {
|
|
503
|
+
return this._vectorStore.getAll().map((t) => t.name);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Number of registered tools
|
|
508
|
+
*/
|
|
509
|
+
get toolCount(): number {
|
|
510
|
+
return this._vectorStore.size + this._pendingTools.length;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Execute a tool by name
|
|
515
|
+
*/
|
|
516
|
+
async execute(
|
|
517
|
+
toolName: string,
|
|
518
|
+
params: Record<string, unknown>
|
|
519
|
+
): Promise<unknown> {
|
|
520
|
+
const tool = this._vectorStore.get(toolName);
|
|
521
|
+
if (!tool) {
|
|
522
|
+
throw new Error(`Tool not found: ${toolName}`);
|
|
523
|
+
}
|
|
524
|
+
if (!tool.handler) {
|
|
525
|
+
throw new Error(`Tool has no handler: ${toolName}`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// PRE_EXECUTE hook
|
|
529
|
+
let ctx = createContext(HookType.PRE_EXECUTE, {
|
|
530
|
+
tool,
|
|
531
|
+
toolName,
|
|
532
|
+
params,
|
|
533
|
+
});
|
|
534
|
+
ctx = await this._hooks.run(HookType.PRE_EXECUTE, ctx);
|
|
535
|
+
if (ctx.cancelled) {
|
|
536
|
+
if (ctx.error) {
|
|
537
|
+
throw ctx.error;
|
|
538
|
+
}
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Validate parameters if enabled
|
|
543
|
+
if (this._validateOnExecute && this._validators) {
|
|
544
|
+
const paramsResult = this._validators.validateParams(tool, params);
|
|
545
|
+
if (!paramsResult.valid) {
|
|
546
|
+
throw new ValidationError(paramsResult);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Execute the handler
|
|
551
|
+
const result = await tool.handler(params);
|
|
552
|
+
|
|
553
|
+
// Validate result if enabled
|
|
554
|
+
if (this._validateOnExecute && this._validators) {
|
|
555
|
+
const resultValidation = this._validators.validateResult(tool, result);
|
|
556
|
+
if (!resultValidation.valid) {
|
|
557
|
+
throw new ValidationError(resultValidation);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// POST_EXECUTE hook
|
|
562
|
+
let postCtx = createContext(HookType.POST_EXECUTE, {
|
|
563
|
+
tool,
|
|
564
|
+
toolName,
|
|
565
|
+
params,
|
|
566
|
+
result,
|
|
567
|
+
});
|
|
568
|
+
postCtx = await this._hooks.run(HookType.POST_EXECUTE, postCtx);
|
|
569
|
+
|
|
570
|
+
return (postCtx.data.result as unknown) ?? result;
|
|
571
|
+
}
|
|
572
|
+
}
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vector store for tool embeddings
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Tool, ToolMatch } from "./types";
|
|
6
|
+
import type { SimilarityFunction, ScoringFunction } from "./scoring";
|
|
7
|
+
import { DEFAULT_SIMILARITY, DEFAULT_SCORING } from "./scoring";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* In-memory vector store for tool embeddings.
|
|
11
|
+
*
|
|
12
|
+
* Uses cosine similarity by default for matching. Supports custom
|
|
13
|
+
* similarity and scoring functions for advanced use cases.
|
|
14
|
+
*
|
|
15
|
+
* For production use with many tools, consider using an external
|
|
16
|
+
* store like Pinecone, Chroma, or Qdrant.
|
|
17
|
+
*/
|
|
18
|
+
export class VectorStore {
|
|
19
|
+
private tools: Tool[] = [];
|
|
20
|
+
private embeddings: number[][] = [];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Add a tool with its embedding to the store
|
|
24
|
+
*/
|
|
25
|
+
add(tool: Tool, embedding: number[]): void {
|
|
26
|
+
tool.embedding = embedding;
|
|
27
|
+
this.tools.push(tool);
|
|
28
|
+
this.embeddings.push(embedding);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Add multiple tools with embeddings
|
|
33
|
+
*/
|
|
34
|
+
addBatch(tools: Tool[], embeddings: number[][]): void {
|
|
35
|
+
for (let i = 0; i < tools.length; i++) {
|
|
36
|
+
this.add(tools[i], embeddings[i]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Search for similar tools
|
|
42
|
+
*
|
|
43
|
+
* @param queryEmbedding - The query embedding vector
|
|
44
|
+
* @param topK - Maximum number of results
|
|
45
|
+
* @param threshold - Minimum score to include
|
|
46
|
+
* @param similarityFn - Custom similarity function (defaults to cosine)
|
|
47
|
+
* @param scoringFn - Custom scoring function (defaults to priority scoring)
|
|
48
|
+
*/
|
|
49
|
+
search(
|
|
50
|
+
queryEmbedding: number[],
|
|
51
|
+
topK: number = 5,
|
|
52
|
+
threshold: number = 0.0,
|
|
53
|
+
similarityFn?: SimilarityFunction,
|
|
54
|
+
scoringFn?: ScoringFunction
|
|
55
|
+
): ToolMatch[] {
|
|
56
|
+
if (this.tools.length === 0) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Use provided functions or defaults
|
|
61
|
+
const simFn = similarityFn ?? DEFAULT_SIMILARITY;
|
|
62
|
+
const scoreFn = scoringFn ?? DEFAULT_SCORING;
|
|
63
|
+
|
|
64
|
+
const matches: ToolMatch[] = [];
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < this.tools.length; i++) {
|
|
67
|
+
const tool = this.tools[i];
|
|
68
|
+
const embedding = this.embeddings[i];
|
|
69
|
+
|
|
70
|
+
// Compute similarity
|
|
71
|
+
const similarity = simFn.compute(queryEmbedding, embedding);
|
|
72
|
+
|
|
73
|
+
// Apply scoring (includes priority boost by default)
|
|
74
|
+
const finalScore = scoreFn.score(similarity, tool);
|
|
75
|
+
|
|
76
|
+
if (finalScore >= threshold) {
|
|
77
|
+
matches.push({ tool, score: finalScore });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Sort by score descending and take top_k
|
|
82
|
+
matches.sort((a, b) => b.score - a.score);
|
|
83
|
+
return matches.slice(0, topK);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Remove a tool by name
|
|
88
|
+
*/
|
|
89
|
+
remove(toolName: string): boolean {
|
|
90
|
+
const index = this.tools.findIndex((t) => t.name === toolName);
|
|
91
|
+
if (index !== -1) {
|
|
92
|
+
this.tools.splice(index, 1);
|
|
93
|
+
this.embeddings.splice(index, 1);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Remove all tools from the store
|
|
101
|
+
*/
|
|
102
|
+
clear(): void {
|
|
103
|
+
this.tools = [];
|
|
104
|
+
this.embeddings = [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get a tool by name
|
|
109
|
+
*/
|
|
110
|
+
get(toolName: string): Tool | undefined {
|
|
111
|
+
return this.tools.find((t) => t.name === toolName);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Number of tools in the store
|
|
116
|
+
*/
|
|
117
|
+
get size(): number {
|
|
118
|
+
return this.tools.length;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Check if a tool exists by name
|
|
123
|
+
*/
|
|
124
|
+
has(toolName: string): boolean {
|
|
125
|
+
return this.tools.some((t) => t.name === toolName);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get all tools
|
|
130
|
+
*/
|
|
131
|
+
getAll(): Tool[] {
|
|
132
|
+
return [...this.tools];
|
|
133
|
+
}
|
|
134
|
+
}
|