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/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
+ }