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