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.
@@ -0,0 +1,87 @@
1
+ /**
2
+ * OpenAI embedding provider
3
+ */
4
+
5
+ import type { EmbeddingProvider } from "./base";
6
+
7
+ interface OpenAIEmbeddingsOptions {
8
+ model?: string;
9
+ apiKey?: string;
10
+ dimensions?: number;
11
+ }
12
+
13
+ const DEFAULT_DIMENSIONS: Record<string, number> = {
14
+ "text-embedding-3-small": 1536,
15
+ "text-embedding-3-large": 3072,
16
+ "text-embedding-ada-002": 1536,
17
+ };
18
+
19
+ export class OpenAIEmbeddings implements EmbeddingProvider {
20
+ private client: any | null = null;
21
+ private model: string;
22
+ private apiKey?: string;
23
+ readonly dimensions: number;
24
+
25
+ constructor(options: OpenAIEmbeddingsOptions = {}) {
26
+ const { model = "text-embedding-3-small", apiKey, dimensions } = options;
27
+
28
+ this.model = model;
29
+ this.apiKey = apiKey;
30
+ this.dimensions = dimensions ?? DEFAULT_DIMENSIONS[model] ?? 1536;
31
+ }
32
+
33
+ private async ensureClient(): Promise<any> {
34
+ if (this.client) {
35
+ return this.client;
36
+ }
37
+
38
+ try {
39
+ const mod: any = await import("openai");
40
+ const OpenAI = mod?.OpenAI ?? mod?.default?.OpenAI ?? mod?.default;
41
+ if (!OpenAI) {
42
+ throw new Error("OpenAI export not found");
43
+ }
44
+ this.client = new OpenAI({ apiKey: this.apiKey });
45
+ return this.client;
46
+ } catch {
47
+ throw new Error(
48
+ "OpenAI provider requires the openai package. Install with: npm install openai"
49
+ );
50
+ }
51
+ }
52
+
53
+ async embed(text: string): Promise<number[]> {
54
+ const client = await this.ensureClient();
55
+ const params: Record<string, unknown> = {
56
+ model: this.model,
57
+ input: text,
58
+ };
59
+
60
+ if (this.model.includes("text-embedding-3")) {
61
+ params.dimensions = this.dimensions;
62
+ }
63
+
64
+ const response = await client.embeddings.create(params);
65
+ return response.data[0].embedding;
66
+ }
67
+
68
+ async embedBatch(texts: string[]): Promise<number[][]> {
69
+ const client = await this.ensureClient();
70
+ const params: Record<string, unknown> = {
71
+ model: this.model,
72
+ input: texts,
73
+ };
74
+
75
+ if (this.model.includes("text-embedding-3")) {
76
+ params.dimensions = this.dimensions;
77
+ }
78
+
79
+ const response = await client.embeddings.create(params);
80
+
81
+ // Sort by index to ensure order matches input
82
+ const sorted = [...response.data].sort(
83
+ (a: { index: number }, b: { index: number }) => a.index - b.index
84
+ );
85
+ return sorted.map((item: { embedding: number[] }) => item.embedding);
86
+ }
87
+ }
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Tool formatters for different LLM providers
3
+ */
4
+
5
+ import type { ResolvedTool, ToolSchema } from "./types";
6
+
7
+ /**
8
+ * Interface for custom tool formatters
9
+ */
10
+ export interface ToolFormatter {
11
+ /** Format name used to identify this formatter */
12
+ readonly name: string;
13
+
14
+ /** Format a resolved tool for LLM consumption */
15
+ format(tool: ResolvedTool): Record<string, unknown>;
16
+
17
+ /** Format multiple tools (override for batch optimizations) */
18
+ formatBatch?(tools: ResolvedTool[]): Record<string, unknown>[];
19
+ }
20
+
21
+ /**
22
+ * Returns the tool schema as-is
23
+ */
24
+ export class RawFormatter implements ToolFormatter {
25
+ readonly name = "raw";
26
+
27
+ format(tool: ResolvedTool): Record<string, unknown> {
28
+ return tool.schema as unknown as Record<string, unknown>;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Formatter for OpenAI function calling format
34
+ */
35
+ export class OpenAIFormatter implements ToolFormatter {
36
+ readonly name = "openai";
37
+
38
+ format(tool: ResolvedTool): Record<string, unknown> {
39
+ return {
40
+ type: "function",
41
+ function: tool.schema,
42
+ };
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Formatter for Anthropic tool use format
48
+ */
49
+ export class AnthropicFormatter implements ToolFormatter {
50
+ readonly name = "anthropic";
51
+
52
+ format(tool: ResolvedTool): Record<string, unknown> {
53
+ return {
54
+ name: tool.schema.name,
55
+ description: tool.schema.description,
56
+ input_schema: tool.schema.parameters,
57
+ };
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Formatter for Google Gemini function calling format
63
+ */
64
+ export class GeminiFormatter implements ToolFormatter {
65
+ readonly name = "gemini";
66
+
67
+ format(tool: ResolvedTool): Record<string, unknown> {
68
+ return {
69
+ name: tool.schema.name,
70
+ description: tool.schema.description,
71
+ parameters: tool.schema.parameters,
72
+ };
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Formatter for Mistral function calling format (OpenAI-compatible)
78
+ */
79
+ export class MistralFormatter implements ToolFormatter {
80
+ readonly name = "mistral";
81
+
82
+ format(tool: ResolvedTool): Record<string, unknown> {
83
+ return {
84
+ type: "function",
85
+ function: {
86
+ name: tool.schema.name,
87
+ description: tool.schema.description,
88
+ parameters: tool.schema.parameters,
89
+ },
90
+ };
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Formatter for Llama/Meta function calling format
96
+ */
97
+ export class LlamaFormatter implements ToolFormatter {
98
+ readonly name = "llama";
99
+
100
+ format(tool: ResolvedTool): Record<string, unknown> {
101
+ return {
102
+ type: "function",
103
+ function: {
104
+ name: tool.schema.name,
105
+ description: tool.schema.description,
106
+ parameters: tool.schema.parameters,
107
+ },
108
+ };
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Formatter for Cohere Command R format
114
+ */
115
+ export class CohereFormatter implements ToolFormatter {
116
+ readonly name = "cohere";
117
+
118
+ format(tool: ResolvedTool): Record<string, unknown> {
119
+ const params = tool.schema.parameters;
120
+ const properties = params.properties as Record<string, Record<string, unknown>>;
121
+ const required = new Set(params.required);
122
+
123
+ const parameterDefinitions: Record<string, unknown> = {};
124
+ for (const [name, prop] of Object.entries(properties)) {
125
+ parameterDefinitions[name] = {
126
+ type: prop.type ?? "string",
127
+ description: prop.description ?? "",
128
+ required: required.has(name),
129
+ };
130
+ }
131
+
132
+ return {
133
+ name: tool.schema.name,
134
+ description: tool.schema.description,
135
+ parameter_definitions: parameterDefinitions,
136
+ };
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Minimal formatter with just name and description
142
+ */
143
+ export class MinimalFormatter implements ToolFormatter {
144
+ readonly name = "minimal";
145
+
146
+ format(tool: ResolvedTool): Record<string, unknown> {
147
+ return {
148
+ name: tool.schema.name,
149
+ description: tool.schema.description,
150
+ };
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Verbose formatter with all available information
156
+ */
157
+ export class VerboseFormatter implements ToolFormatter {
158
+ readonly name = "verbose";
159
+
160
+ format(tool: ResolvedTool): Record<string, unknown> {
161
+ return {
162
+ name: tool.name,
163
+ schema: tool.schema,
164
+ score: tool.score,
165
+ hasHandler: tool.handler !== undefined,
166
+ };
167
+ }
168
+ }
169
+
170
+ // Global formatter registry
171
+ const formatters: Map<string, ToolFormatter> = new Map();
172
+
173
+ /**
174
+ * Register a custom formatter
175
+ */
176
+ export function registerFormatter(formatter: ToolFormatter): void {
177
+ formatters.set(formatter.name, formatter);
178
+ }
179
+
180
+ /**
181
+ * Unregister a formatter by name
182
+ */
183
+ export function unregisterFormatter(name: string): boolean {
184
+ return formatters.delete(name);
185
+ }
186
+
187
+ /**
188
+ * Get a formatter by name
189
+ */
190
+ export function getFormatter(name: string): ToolFormatter {
191
+ const formatter = formatters.get(name);
192
+ if (!formatter) {
193
+ const available = Array.from(formatters.keys()).join(", ");
194
+ throw new Error(`Unknown formatter '${name}'. Available: ${available}`);
195
+ }
196
+ return formatter;
197
+ }
198
+
199
+ /**
200
+ * List all registered formatter names
201
+ */
202
+ export function listFormatters(): string[] {
203
+ return Array.from(formatters.keys());
204
+ }
205
+
206
+ /**
207
+ * Check if a formatter is registered
208
+ */
209
+ export function hasFormatter(name: string): boolean {
210
+ return formatters.has(name);
211
+ }
212
+
213
+ /**
214
+ * Format tools using a formatter (by name or instance)
215
+ */
216
+ export function formatTools(
217
+ tools: ResolvedTool[],
218
+ formatter: string | ToolFormatter
219
+ ): Record<string, unknown>[] {
220
+ const fmt = typeof formatter === "string" ? getFormatter(formatter) : formatter;
221
+
222
+ if (fmt.formatBatch) {
223
+ return fmt.formatBatch(tools);
224
+ }
225
+
226
+ return tools.map((tool) => fmt.format(tool));
227
+ }
228
+
229
+ // Register default formatters
230
+ function registerDefaults(): void {
231
+ const defaults: ToolFormatter[] = [
232
+ new RawFormatter(),
233
+ new OpenAIFormatter(),
234
+ new AnthropicFormatter(),
235
+ new GeminiFormatter(),
236
+ new MistralFormatter(),
237
+ new LlamaFormatter(),
238
+ new CohereFormatter(),
239
+ new MinimalFormatter(),
240
+ new VerboseFormatter(),
241
+ ];
242
+
243
+ for (const formatter of defaults) {
244
+ registerFormatter(formatter);
245
+ }
246
+ }
247
+
248
+ // Initialize default formatters
249
+ registerDefaults();
package/src/hooks.ts ADDED
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Middleware/hooks system for SIS
3
+ */
4
+
5
+ import type { Tool, ResolvedTool, ToolMatch } from "./types";
6
+
7
+ /**
8
+ * Types of hooks available in the SIS lifecycle
9
+ */
10
+ export enum HookType {
11
+ // Registration hooks
12
+ PRE_REGISTER = "pre_register",
13
+ POST_REGISTER = "post_register",
14
+
15
+ // Initialization hooks
16
+ PRE_EMBED = "pre_embed",
17
+ POST_EMBED = "post_embed",
18
+
19
+ // Resolution hooks
20
+ PRE_RESOLVE = "pre_resolve",
21
+ POST_RESOLVE = "post_resolve",
22
+ PRE_SEARCH = "pre_search",
23
+ POST_SEARCH = "post_search",
24
+
25
+ // Execution hooks
26
+ PRE_EXECUTE = "pre_execute",
27
+ POST_EXECUTE = "post_execute",
28
+ }
29
+
30
+ /**
31
+ * Context passed to hooks
32
+ */
33
+ export interface HookContext {
34
+ hookType: HookType;
35
+ data: Record<string, unknown>;
36
+ cancelled: boolean;
37
+ error: Error | null;
38
+ }
39
+
40
+ /**
41
+ * Create a new hook context
42
+ */
43
+ export function createContext(
44
+ hookType: HookType,
45
+ data: Record<string, unknown> = {}
46
+ ): HookContext {
47
+ return {
48
+ hookType,
49
+ data,
50
+ cancelled: false,
51
+ error: null,
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Cancel further processing
57
+ */
58
+ export function cancelContext(context: HookContext, reason?: string): void {
59
+ context.cancelled = true;
60
+ if (reason) {
61
+ context.data.cancelReason = reason;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Set an error on the context
67
+ */
68
+ export function setContextError(context: HookContext, error: Error): void {
69
+ context.error = error;
70
+ context.cancelled = true;
71
+ }
72
+
73
+ /**
74
+ * Interface for hooks
75
+ */
76
+ export interface Hook {
77
+ /** The hook type this hook handles */
78
+ hookType: HookType;
79
+
80
+ /** Hook priority. Higher priority hooks run first. */
81
+ priority?: number;
82
+
83
+ /** Execute the hook */
84
+ execute(context: HookContext): Promise<HookContext> | HookContext;
85
+ }
86
+
87
+ /**
88
+ * Hook function type
89
+ */
90
+ export type HookFunction = (
91
+ context: HookContext
92
+ ) => Promise<HookContext> | HookContext;
93
+
94
+ /**
95
+ * Create a hook from a function
96
+ */
97
+ export function createHook(
98
+ hookType: HookType,
99
+ fn: HookFunction,
100
+ priority: number = 0
101
+ ): Hook {
102
+ return {
103
+ hookType,
104
+ priority,
105
+ execute: fn,
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Registry for managing hooks
111
+ */
112
+ export class HookRegistry {
113
+ private hooks: Map<HookType, Hook[]> = new Map();
114
+
115
+ constructor() {
116
+ // Initialize all hook types
117
+ for (const type of Object.values(HookType)) {
118
+ this.hooks.set(type, []);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Register a hook
124
+ */
125
+ register(hook: Hook): void {
126
+ const hooks = this.hooks.get(hook.hookType) ?? [];
127
+ hooks.push(hook);
128
+ // Sort by priority (highest first)
129
+ hooks.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
130
+ this.hooks.set(hook.hookType, hooks);
131
+ }
132
+
133
+ /**
134
+ * Register a function as a hook
135
+ */
136
+ on(
137
+ hookType: HookType,
138
+ fn: HookFunction,
139
+ priority: number = 0
140
+ ): Hook {
141
+ const hook = createHook(hookType, fn, priority);
142
+ this.register(hook);
143
+ return hook;
144
+ }
145
+
146
+ /**
147
+ * Unregister a hook
148
+ */
149
+ unregister(hook: Hook): boolean {
150
+ const hooks = this.hooks.get(hook.hookType) ?? [];
151
+ const index = hooks.indexOf(hook);
152
+ if (index !== -1) {
153
+ hooks.splice(index, 1);
154
+ return true;
155
+ }
156
+ return false;
157
+ }
158
+
159
+ /**
160
+ * Clear hooks
161
+ */
162
+ clear(hookType?: HookType): void {
163
+ if (hookType) {
164
+ this.hooks.set(hookType, []);
165
+ } else {
166
+ for (const type of Object.values(HookType)) {
167
+ this.hooks.set(type, []);
168
+ }
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Get all hooks for a type
174
+ */
175
+ getHooks(hookType: HookType): Hook[] {
176
+ return [...(this.hooks.get(hookType) ?? [])];
177
+ }
178
+
179
+ /**
180
+ * Check if any hooks are registered for a type
181
+ */
182
+ hasHooks(hookType: HookType): boolean {
183
+ return (this.hooks.get(hookType) ?? []).length > 0;
184
+ }
185
+
186
+ /**
187
+ * Run all hooks of a given type
188
+ */
189
+ async run(hookType: HookType, context: HookContext): Promise<HookContext> {
190
+ const hooks = this.hooks.get(hookType) ?? [];
191
+
192
+ for (const hook of hooks) {
193
+ if (context.cancelled) {
194
+ break;
195
+ }
196
+
197
+ try {
198
+ const result = hook.execute(context);
199
+ if (result instanceof Promise) {
200
+ context = await result;
201
+ } else {
202
+ context = result;
203
+ }
204
+ } catch (error) {
205
+ setContextError(context, error as Error);
206
+ break;
207
+ }
208
+ }
209
+
210
+ return context;
211
+ }
212
+ }
213
+
214
+ // Pre-built hook implementations for common use cases
215
+
216
+ /**
217
+ * Hook that logs hook execution
218
+ */
219
+ export class LoggingHook implements Hook {
220
+ hookType: HookType;
221
+ priority = 0;
222
+ private logger: (message: string) => void;
223
+
224
+ constructor(hookType: HookType, logger?: (message: string) => void) {
225
+ this.hookType = hookType;
226
+ this.logger = logger ?? console.log;
227
+ }
228
+
229
+ execute(context: HookContext): HookContext {
230
+ this.logger(`[${this.hookType}] data=${JSON.stringify(context.data)}`);
231
+ return context;
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Hook that records execution time
237
+ */
238
+ export class TimingHook implements Hook {
239
+ hookType: HookType;
240
+ priority = 0;
241
+ private preHook: HookType;
242
+ private postHook: HookType;
243
+ private timingKey: string;
244
+
245
+ constructor(
246
+ preHook: HookType,
247
+ postHook: HookType,
248
+ timingKey: string = "timing"
249
+ ) {
250
+ this.preHook = preHook;
251
+ this.postHook = postHook;
252
+ this.hookType = preHook; // Will be updated when used
253
+ this.timingKey = timingKey;
254
+ }
255
+
256
+ execute(context: HookContext): HookContext {
257
+ if (context.hookType === this.preHook) {
258
+ context.data[`${this.timingKey}Start`] = performance.now();
259
+ } else if (context.hookType === this.postHook) {
260
+ const start = context.data[`${this.timingKey}Start`] as number;
261
+ if (start) {
262
+ const duration = performance.now() - start;
263
+ context.data[`${this.timingKey}Ms`] = duration;
264
+ }
265
+ }
266
+ return context;
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Hook that collects metrics
272
+ */
273
+ export class MetricsHook implements Hook {
274
+ hookType: HookType;
275
+ priority = 0;
276
+ count = 0;
277
+ errors = 0;
278
+
279
+ constructor(hookType: HookType) {
280
+ this.hookType = hookType;
281
+ }
282
+
283
+ execute(context: HookContext): HookContext {
284
+ this.count++;
285
+ if (context.error) {
286
+ this.errors++;
287
+ }
288
+ return context;
289
+ }
290
+
291
+ reset(): void {
292
+ this.count = 0;
293
+ this.errors = 0;
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Hook that caches resolution results
299
+ */
300
+ export class CachingHook implements Hook {
301
+ hookType = HookType.PRE_RESOLVE;
302
+ priority = 100; // Run early to check cache
303
+ private cache: Map<string, unknown[]> = new Map();
304
+ private maxSize: number;
305
+
306
+ constructor(maxSize: number = 100) {
307
+ this.maxSize = maxSize;
308
+ }
309
+
310
+ execute(context: HookContext): HookContext {
311
+ if (context.hookType === HookType.PRE_RESOLVE) {
312
+ const query = context.data.query as string;
313
+ if (query && this.cache.has(query)) {
314
+ context.data.cachedResults = this.cache.get(query);
315
+ context.data.cacheHit = true;
316
+ }
317
+ } else if (context.hookType === HookType.POST_RESOLVE) {
318
+ const query = context.data.query as string;
319
+ const results = context.data.results as unknown[];
320
+ if (query && results && !this.cache.has(query)) {
321
+ if (this.cache.size >= this.maxSize) {
322
+ // Remove oldest entry
323
+ const firstKey = this.cache.keys().next().value;
324
+ if (firstKey) {
325
+ this.cache.delete(firstKey);
326
+ }
327
+ }
328
+ this.cache.set(query, results);
329
+ }
330
+ }
331
+ return context;
332
+ }
333
+
334
+ clearCache(): void {
335
+ this.cache.clear();
336
+ }
337
+
338
+ get cacheSize(): number {
339
+ return this.cache.size;
340
+ }
341
+ }