@artemiskit/sdk 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.
@@ -0,0 +1,381 @@
1
+ /**
2
+ * Guardian Interceptor
3
+ *
4
+ * Wraps LLM clients to intercept and validate all inputs/outputs.
5
+ * Acts as middleware between the agent and the LLM.
6
+ */
7
+
8
+ import type { GenerateOptions, GenerateResult, ModelClient } from '@artemiskit/core';
9
+ import { nanoid } from 'nanoid';
10
+ import type {
11
+ GuardianEvent,
12
+ GuardianEventHandler,
13
+ GuardrailResult,
14
+ InterceptedRequest,
15
+ InterceptedResponse,
16
+ Violation,
17
+ } from './types';
18
+
19
+ /**
20
+ * Guardrail function signature
21
+ */
22
+ export type GuardrailFn = (
23
+ content: string,
24
+ context?: Record<string, unknown>
25
+ ) => Promise<GuardrailResult>;
26
+
27
+ /**
28
+ * Interceptor configuration
29
+ */
30
+ export interface InterceptorConfig {
31
+ /** Enable input validation */
32
+ validateInput?: boolean;
33
+ /** Enable output validation */
34
+ validateOutput?: boolean;
35
+ /** Input guardrails to run */
36
+ inputGuardrails?: GuardrailFn[];
37
+ /** Output guardrails to run */
38
+ outputGuardrails?: GuardrailFn[];
39
+ /** Whether to block on validation failure */
40
+ blockOnFailure?: boolean;
41
+ /** Event handlers */
42
+ onEvent?: GuardianEventHandler;
43
+ /** Log violations */
44
+ logViolations?: boolean;
45
+ }
46
+
47
+ /**
48
+ * Interceptor statistics
49
+ */
50
+ export interface InterceptorStats {
51
+ totalRequests: number;
52
+ blockedRequests: number;
53
+ totalViolations: number;
54
+ averageLatencyMs: number;
55
+ inputViolations: number;
56
+ outputViolations: number;
57
+ }
58
+
59
+ /**
60
+ * Guardian Interceptor wraps a ModelClient to add validation
61
+ */
62
+ export class GuardianInterceptor implements ModelClient {
63
+ readonly provider: string;
64
+ private client: ModelClient;
65
+ private config: InterceptorConfig;
66
+ private stats: InterceptorStats;
67
+ private requestHistory: Map<string, InterceptedRequest>;
68
+
69
+ constructor(client: ModelClient, config: InterceptorConfig = {}) {
70
+ this.client = client;
71
+ this.provider = client.provider;
72
+ this.config = {
73
+ validateInput: true,
74
+ validateOutput: true,
75
+ blockOnFailure: true,
76
+ logViolations: true,
77
+ ...config,
78
+ };
79
+ this.stats = {
80
+ totalRequests: 0,
81
+ blockedRequests: 0,
82
+ totalViolations: 0,
83
+ averageLatencyMs: 0,
84
+ inputViolations: 0,
85
+ outputViolations: 0,
86
+ };
87
+ this.requestHistory = new Map();
88
+ }
89
+
90
+ /**
91
+ * Generate with guardrail validation
92
+ */
93
+ async generate(options: GenerateOptions): Promise<GenerateResult> {
94
+ const requestId = nanoid();
95
+ const startTime = Date.now();
96
+ this.stats.totalRequests++;
97
+
98
+ // Extract prompt text
99
+ const promptText = this.extractPromptText(options.prompt);
100
+
101
+ // Create intercepted request record
102
+ const request: InterceptedRequest = {
103
+ id: requestId,
104
+ input: promptText,
105
+ metadata: options.metadata,
106
+ timestamp: new Date(),
107
+ };
108
+ this.requestHistory.set(requestId, request);
109
+
110
+ // Emit request start event
111
+ this.emitEvent({
112
+ type: 'request_start',
113
+ timestamp: new Date(),
114
+ data: { requestId, prompt: promptText },
115
+ });
116
+
117
+ // Run input validation
118
+ if (this.config.validateInput && this.config.inputGuardrails) {
119
+ const inputResult = await this.runGuardrails(promptText, this.config.inputGuardrails, {
120
+ requestId,
121
+ phase: 'input',
122
+ });
123
+
124
+ if (!inputResult.passed) {
125
+ this.stats.inputViolations += inputResult.violations.length;
126
+ this.stats.totalViolations += inputResult.violations.length;
127
+
128
+ for (const violation of inputResult.violations) {
129
+ this.emitEvent({
130
+ type: 'violation_detected',
131
+ timestamp: new Date(),
132
+ data: { requestId, phase: 'input', violation },
133
+ });
134
+ }
135
+
136
+ if (this.config.blockOnFailure && inputResult.violations.some((v) => v.blocked)) {
137
+ this.stats.blockedRequests++;
138
+ this.emitEvent({
139
+ type: 'request_blocked',
140
+ timestamp: new Date(),
141
+ data: { requestId, phase: 'input', violations: inputResult.violations },
142
+ });
143
+
144
+ throw new GuardianBlockedError('Input validation failed', inputResult.violations);
145
+ }
146
+ }
147
+ }
148
+
149
+ // Call the underlying client
150
+ const result = await this.client.generate(options);
151
+
152
+ // Create intercepted response record
153
+ const response: InterceptedResponse = {
154
+ id: nanoid(),
155
+ requestId,
156
+ output: result.text,
157
+ metadata: result.raw as Record<string, unknown> | undefined,
158
+ timestamp: new Date(),
159
+ latencyMs: result.latencyMs,
160
+ };
161
+
162
+ // Run output validation
163
+ if (this.config.validateOutput && this.config.outputGuardrails) {
164
+ const outputResult = await this.runGuardrails(result.text, this.config.outputGuardrails, {
165
+ requestId,
166
+ phase: 'output',
167
+ response,
168
+ });
169
+
170
+ if (!outputResult.passed) {
171
+ this.stats.outputViolations += outputResult.violations.length;
172
+ this.stats.totalViolations += outputResult.violations.length;
173
+
174
+ for (const violation of outputResult.violations) {
175
+ this.emitEvent({
176
+ type: 'violation_detected',
177
+ timestamp: new Date(),
178
+ data: { requestId, phase: 'output', violation },
179
+ });
180
+ }
181
+
182
+ if (this.config.blockOnFailure && outputResult.violations.some((v) => v.blocked)) {
183
+ this.stats.blockedRequests++;
184
+ this.emitEvent({
185
+ type: 'request_blocked',
186
+ timestamp: new Date(),
187
+ data: { requestId, phase: 'output', violations: outputResult.violations },
188
+ });
189
+
190
+ throw new GuardianBlockedError('Output validation failed', outputResult.violations);
191
+ }
192
+
193
+ // If output was transformed, return the transformed content
194
+ if (outputResult.transformedContent) {
195
+ return {
196
+ ...result,
197
+ text: outputResult.transformedContent,
198
+ };
199
+ }
200
+ }
201
+ }
202
+
203
+ // Update average latency
204
+ const totalLatency = this.stats.averageLatencyMs * (this.stats.totalRequests - 1);
205
+ this.stats.averageLatencyMs = (totalLatency + result.latencyMs) / this.stats.totalRequests;
206
+
207
+ // Emit request complete event
208
+ this.emitEvent({
209
+ type: 'request_complete',
210
+ timestamp: new Date(),
211
+ data: {
212
+ requestId,
213
+ latencyMs: Date.now() - startTime,
214
+ response: result.text,
215
+ },
216
+ });
217
+
218
+ return result;
219
+ }
220
+
221
+ /**
222
+ * Get model capabilities (pass-through)
223
+ */
224
+ async capabilities() {
225
+ return this.client.capabilities();
226
+ }
227
+
228
+ /**
229
+ * Stream generation (pass-through with validation)
230
+ */
231
+ stream?(options: GenerateOptions, onChunk: (chunk: string) => void): AsyncIterable<string> {
232
+ if (!this.client.stream) {
233
+ throw new Error('Underlying client does not support streaming');
234
+ }
235
+ // For streaming, we'd need to accumulate and validate
236
+ // This is a simplified pass-through for now
237
+ return this.client.stream(options, onChunk);
238
+ }
239
+
240
+ /**
241
+ * Embed text (pass-through)
242
+ */
243
+ async embed?(text: string, model?: string): Promise<number[]> {
244
+ if (!this.client.embed) {
245
+ throw new Error('Underlying client does not support embeddings');
246
+ }
247
+ return this.client.embed(text, model);
248
+ }
249
+
250
+ /**
251
+ * Close the client
252
+ */
253
+ async close?(): Promise<void> {
254
+ if (this.client.close) {
255
+ return this.client.close();
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Get interceptor statistics
261
+ */
262
+ getStats(): InterceptorStats {
263
+ return { ...this.stats };
264
+ }
265
+
266
+ /**
267
+ * Reset statistics
268
+ */
269
+ resetStats(): void {
270
+ this.stats = {
271
+ totalRequests: 0,
272
+ blockedRequests: 0,
273
+ totalViolations: 0,
274
+ averageLatencyMs: 0,
275
+ inputViolations: 0,
276
+ outputViolations: 0,
277
+ };
278
+ }
279
+
280
+ /**
281
+ * Get request history
282
+ */
283
+ getRequestHistory(): InterceptedRequest[] {
284
+ return Array.from(this.requestHistory.values());
285
+ }
286
+
287
+ /**
288
+ * Clear request history
289
+ */
290
+ clearHistory(): void {
291
+ this.requestHistory.clear();
292
+ }
293
+
294
+ /**
295
+ * Run guardrails and aggregate results
296
+ */
297
+ private async runGuardrails(
298
+ content: string,
299
+ guardrails: GuardrailFn[],
300
+ context: Record<string, unknown>
301
+ ): Promise<GuardrailResult> {
302
+ const allViolations: Violation[] = [];
303
+ let transformedContent: string | undefined;
304
+ let currentContent = content;
305
+
306
+ for (const guardrail of guardrails) {
307
+ const result = await guardrail(currentContent, context);
308
+
309
+ if (!result.passed) {
310
+ allViolations.push(...result.violations);
311
+ }
312
+
313
+ // Apply transformation if provided
314
+ if (result.transformedContent) {
315
+ transformedContent = result.transformedContent;
316
+ currentContent = result.transformedContent;
317
+ }
318
+ }
319
+
320
+ return {
321
+ passed: allViolations.length === 0,
322
+ violations: allViolations,
323
+ transformedContent,
324
+ };
325
+ }
326
+
327
+ /**
328
+ * Extract text from prompt (string or messages array)
329
+ */
330
+ private extractPromptText(prompt: GenerateOptions['prompt']): string {
331
+ if (typeof prompt === 'string') {
332
+ return prompt;
333
+ }
334
+ return prompt.map((m) => m.content).join('\n');
335
+ }
336
+
337
+ /**
338
+ * Emit an event to the handler
339
+ */
340
+ private emitEvent(event: GuardianEvent): void {
341
+ if (this.config.onEvent) {
342
+ try {
343
+ this.config.onEvent(event);
344
+ } catch (err) {
345
+ if (this.config.logViolations) {
346
+ console.error('Error in guardian event handler:', err);
347
+ }
348
+ }
349
+ }
350
+
351
+ if (this.config.logViolations && event.type === 'violation_detected') {
352
+ const violation = event.data.violation as Violation;
353
+ console.warn(
354
+ `[Guardian] Violation detected: ${violation.type} - ${violation.message} (${violation.severity})`
355
+ );
356
+ }
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Error thrown when a request is blocked by guardrails
362
+ */
363
+ export class GuardianBlockedError extends Error {
364
+ readonly violations: Violation[];
365
+
366
+ constructor(message: string, violations: Violation[]) {
367
+ super(message);
368
+ this.name = 'GuardianBlockedError';
369
+ this.violations = violations;
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Create an interceptor wrapping a model client
375
+ */
376
+ export function createInterceptor(
377
+ client: ModelClient,
378
+ config: InterceptorConfig = {}
379
+ ): GuardianInterceptor {
380
+ return new GuardianInterceptor(client, config);
381
+ }