@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.
- package/CHANGELOG.md +134 -0
- package/README.md +173 -0
- package/adapters/openai/dist/index.js +5625 -0
- package/dist/index.js +42577 -0
- package/dist/matchers/index.js +224 -0
- package/dist/matchers/jest.js +257 -0
- package/dist/matchers/vitest.js +257 -0
- package/package.json +78 -0
- package/src/__tests__/artemiskit.test.ts +425 -0
- package/src/__tests__/matchers.test.ts +450 -0
- package/src/artemiskit.ts +791 -0
- package/src/guardian/action-validator.ts +585 -0
- package/src/guardian/circuit-breaker.ts +655 -0
- package/src/guardian/guardian.ts +497 -0
- package/src/guardian/guardrails.ts +536 -0
- package/src/guardian/index.ts +142 -0
- package/src/guardian/intent-classifier.ts +378 -0
- package/src/guardian/interceptor.ts +381 -0
- package/src/guardian/policy.ts +446 -0
- package/src/guardian/types.ts +436 -0
- package/src/index.ts +164 -0
- package/src/matchers/core.ts +315 -0
- package/src/matchers/index.ts +26 -0
- package/src/matchers/jest.ts +112 -0
- package/src/matchers/vitest.ts +84 -0
- package/src/types.ts +259 -0
- package/tsconfig.json +11 -0
|
@@ -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
|
+
}
|