@framers/agentos-ext-ml-classifiers 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/LICENSE +23 -0
- package/dist/ClassifierOrchestrator.d.ts +126 -0
- package/dist/ClassifierOrchestrator.d.ts.map +1 -0
- package/dist/ClassifierOrchestrator.js +239 -0
- package/dist/ClassifierOrchestrator.js.map +1 -0
- package/dist/IContentClassifier.d.ts +117 -0
- package/dist/IContentClassifier.d.ts.map +1 -0
- package/dist/IContentClassifier.js +22 -0
- package/dist/IContentClassifier.js.map +1 -0
- package/dist/MLClassifierGuardrail.d.ts +163 -0
- package/dist/MLClassifierGuardrail.d.ts.map +1 -0
- package/dist/MLClassifierGuardrail.js +335 -0
- package/dist/MLClassifierGuardrail.js.map +1 -0
- package/dist/SlidingWindowBuffer.d.ts +213 -0
- package/dist/SlidingWindowBuffer.d.ts.map +1 -0
- package/dist/SlidingWindowBuffer.js +246 -0
- package/dist/SlidingWindowBuffer.js.map +1 -0
- package/dist/classifiers/InjectionClassifier.d.ts +126 -0
- package/dist/classifiers/InjectionClassifier.d.ts.map +1 -0
- package/dist/classifiers/InjectionClassifier.js +210 -0
- package/dist/classifiers/InjectionClassifier.js.map +1 -0
- package/dist/classifiers/JailbreakClassifier.d.ts +124 -0
- package/dist/classifiers/JailbreakClassifier.d.ts.map +1 -0
- package/dist/classifiers/JailbreakClassifier.js +208 -0
- package/dist/classifiers/JailbreakClassifier.js.map +1 -0
- package/dist/classifiers/ToxicityClassifier.d.ts +125 -0
- package/dist/classifiers/ToxicityClassifier.d.ts.map +1 -0
- package/dist/classifiers/ToxicityClassifier.js +212 -0
- package/dist/classifiers/ToxicityClassifier.js.map +1 -0
- package/dist/classifiers/WorkerClassifierProxy.d.ts +158 -0
- package/dist/classifiers/WorkerClassifierProxy.d.ts.map +1 -0
- package/dist/classifiers/WorkerClassifierProxy.js +268 -0
- package/dist/classifiers/WorkerClassifierProxy.js.map +1 -0
- package/dist/index.d.ts +110 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +342 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/ClassifyContentTool.d.ts +105 -0
- package/dist/tools/ClassifyContentTool.d.ts.map +1 -0
- package/dist/tools/ClassifyContentTool.js +149 -0
- package/dist/tools/ClassifyContentTool.js.map +1 -0
- package/dist/types.d.ts +319 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +62 -0
- package/dist/types.js.map +1 -0
- package/dist/worker/classifier-worker.d.ts +49 -0
- package/dist/worker/classifier-worker.d.ts.map +1 -0
- package/dist/worker/classifier-worker.js +180 -0
- package/dist/worker/classifier-worker.js.map +1 -0
- package/package.json +45 -0
- package/src/ClassifierOrchestrator.ts +290 -0
- package/src/IContentClassifier.ts +124 -0
- package/src/MLClassifierGuardrail.ts +419 -0
- package/src/SlidingWindowBuffer.ts +384 -0
- package/src/classifiers/InjectionClassifier.ts +261 -0
- package/src/classifiers/JailbreakClassifier.ts +259 -0
- package/src/classifiers/ToxicityClassifier.ts +263 -0
- package/src/classifiers/WorkerClassifierProxy.ts +366 -0
- package/src/index.ts +383 -0
- package/src/tools/ClassifyContentTool.ts +201 -0
- package/src/types.ts +391 -0
- package/src/worker/classifier-worker.ts +267 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview WorkerClassifierProxy — wraps an IContentClassifier to run
|
|
3
|
+
* inference inside a Web Worker, with automatic main-thread fallback.
|
|
4
|
+
*
|
|
5
|
+
* ## Why a proxy?
|
|
6
|
+
* ML inference (even quantized ONNX / WASM pipelines) can block the main
|
|
7
|
+
* thread for 50–500 ms per classification. Moving classification into a
|
|
8
|
+
* Web Worker keeps the UI responsive. This proxy makes the switch
|
|
9
|
+
* transparent to callers: they still call `classify(text)` and receive a
|
|
10
|
+
* `ClassificationResult`; the underlying transport (Worker vs. direct call)
|
|
11
|
+
* is an implementation detail.
|
|
12
|
+
*
|
|
13
|
+
* ## Fallback policy
|
|
14
|
+
* The proxy falls back to direct (main-thread) delegation whenever:
|
|
15
|
+
* - The global `Worker` constructor is undefined (Node.js, older browsers).
|
|
16
|
+
* - `browserConfig.useWebWorker` is explicitly `false`.
|
|
17
|
+
* - Worker creation throws (e.g. strict CSP that blocks `blob:` URLs).
|
|
18
|
+
*
|
|
19
|
+
* Once a fallback has been triggered by a Worker creation error the proxy
|
|
20
|
+
* sets `workerFailed = true` and remains in fallback mode for all subsequent
|
|
21
|
+
* calls.
|
|
22
|
+
*
|
|
23
|
+
* ## IContentClassifier contract
|
|
24
|
+
* The proxy forwards all identity fields (`id`, `displayName`, `description`,
|
|
25
|
+
* `modelId`) and the `isLoaded` state directly from the wrapped classifier so
|
|
26
|
+
* it is completely transparent to the orchestrator.
|
|
27
|
+
*
|
|
28
|
+
* @module agentos/extensions/packs/ml-classifiers/classifiers/WorkerClassifierProxy
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import type { ClassificationResult } from '@framers/agentos';
|
|
32
|
+
import type { IContentClassifier } from '../IContentClassifier';
|
|
33
|
+
import type { BrowserConfig } from '../types';
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Internal message shapes
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Message sent from the main thread to the Worker to request classification.
|
|
41
|
+
*
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
44
|
+
interface WorkerClassifyRequest {
|
|
45
|
+
/** Discriminant tag that identifies this message type. */
|
|
46
|
+
type: 'classify';
|
|
47
|
+
|
|
48
|
+
/** The text to classify. Passed directly to the pipeline. */
|
|
49
|
+
text: string;
|
|
50
|
+
|
|
51
|
+
/** Hugging Face model ID (or local path) to load if not yet cached. */
|
|
52
|
+
modelId: string;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Whether to request a quantized model variant.
|
|
56
|
+
* Passed through to the `@huggingface/transformers` pipeline factory.
|
|
57
|
+
*/
|
|
58
|
+
quantized: boolean;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* HuggingFace pipeline task string, e.g. `'text-classification'`.
|
|
62
|
+
* Sent so the Worker can use the correct pipeline type when loading the
|
|
63
|
+
* model for the first time.
|
|
64
|
+
*/
|
|
65
|
+
taskType: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Success response posted back from the Worker.
|
|
70
|
+
*
|
|
71
|
+
* @internal
|
|
72
|
+
*/
|
|
73
|
+
interface WorkerResultMessage {
|
|
74
|
+
/** Discriminant tag. */
|
|
75
|
+
type: 'result';
|
|
76
|
+
|
|
77
|
+
/** The resolved classification result. */
|
|
78
|
+
result: ClassificationResult;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Error response posted back from the Worker.
|
|
83
|
+
*
|
|
84
|
+
* @internal
|
|
85
|
+
*/
|
|
86
|
+
interface WorkerErrorMessage {
|
|
87
|
+
/** Discriminant tag. */
|
|
88
|
+
type: 'error';
|
|
89
|
+
|
|
90
|
+
/** Human-readable error message. */
|
|
91
|
+
error: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Union of all possible messages coming back from the Worker. */
|
|
95
|
+
type WorkerResponse = WorkerResultMessage | WorkerErrorMessage;
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// WorkerClassifierProxy
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Transparent proxy around an {@link IContentClassifier} that offloads
|
|
103
|
+
* `classify()` calls to a Web Worker when the browser environment supports it.
|
|
104
|
+
*
|
|
105
|
+
* In all other environments (Node.js, strict CSP, explicit opt-out) the proxy
|
|
106
|
+
* delegates calls directly to the wrapped classifier on the main thread.
|
|
107
|
+
*
|
|
108
|
+
* @implements {IContentClassifier}
|
|
109
|
+
*
|
|
110
|
+
* @example Browser context — Web Worker path
|
|
111
|
+
* ```typescript
|
|
112
|
+
* const toxicity = new ToxicityClassifier(serviceRegistry);
|
|
113
|
+
* const proxy = new WorkerClassifierProxy(toxicity, { useWebWorker: true });
|
|
114
|
+
* const result = await proxy.classify('some text');
|
|
115
|
+
* ```
|
|
116
|
+
*
|
|
117
|
+
* @example Node.js / forced fallback path
|
|
118
|
+
* ```typescript
|
|
119
|
+
* const proxy = new WorkerClassifierProxy(toxicity, { useWebWorker: false });
|
|
120
|
+
* // Delegates directly to toxicity.classify() on the same thread.
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
export class WorkerClassifierProxy implements IContentClassifier {
|
|
124
|
+
// -------------------------------------------------------------------------
|
|
125
|
+
// IContentClassifier identity — delegated from wrapped classifier
|
|
126
|
+
// -------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* {@inheritDoc IContentClassifier.id}
|
|
130
|
+
* Delegated from the wrapped classifier so this proxy is transparent in
|
|
131
|
+
* the orchestrator's service-ID lookups.
|
|
132
|
+
*/
|
|
133
|
+
get id(): string {
|
|
134
|
+
return this.wrapped.id;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* {@inheritDoc IContentClassifier.displayName}
|
|
139
|
+
* Returns the wrapped classifier's display name with a `(Worker)` suffix
|
|
140
|
+
* when the Web Worker path is active, so logs clearly indicate the mode.
|
|
141
|
+
*/
|
|
142
|
+
get displayName(): string {
|
|
143
|
+
return this.wrapped.displayName;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* {@inheritDoc IContentClassifier.description}
|
|
148
|
+
* Delegated directly from the wrapped classifier.
|
|
149
|
+
*/
|
|
150
|
+
get description(): string {
|
|
151
|
+
return this.wrapped.description;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* {@inheritDoc IContentClassifier.modelId}
|
|
156
|
+
* Delegated directly from the wrapped classifier.
|
|
157
|
+
*/
|
|
158
|
+
get modelId(): string {
|
|
159
|
+
return this.wrapped.modelId;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* {@inheritDoc IContentClassifier.isLoaded}
|
|
164
|
+
*
|
|
165
|
+
* Reflects the wrapped classifier's `isLoaded` state. The wrapped
|
|
166
|
+
* instance is the authoritative source because it owns the model weights
|
|
167
|
+
* (whether they live in the Worker or on the main thread).
|
|
168
|
+
*/
|
|
169
|
+
get isLoaded(): boolean {
|
|
170
|
+
return this.wrapped.isLoaded;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* IContentClassifier requires `isLoaded` to be settable via the interface
|
|
175
|
+
* contract (`isLoaded: boolean`). We store the value through the wrapped
|
|
176
|
+
* classifier so the authoritative state lives in one place.
|
|
177
|
+
*/
|
|
178
|
+
set isLoaded(value: boolean) {
|
|
179
|
+
this.wrapped.isLoaded = value;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// -------------------------------------------------------------------------
|
|
183
|
+
// Internal state
|
|
184
|
+
// -------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Set to `true` after a Worker creation failure. Once set, all subsequent
|
|
188
|
+
* `classify()` calls are routed directly to the wrapped classifier without
|
|
189
|
+
* attempting to re-create the Worker.
|
|
190
|
+
*/
|
|
191
|
+
private workerFailed = false;
|
|
192
|
+
|
|
193
|
+
// -------------------------------------------------------------------------
|
|
194
|
+
// Constructor
|
|
195
|
+
// -------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Create a WorkerClassifierProxy.
|
|
199
|
+
*
|
|
200
|
+
* @param wrapped - The real classifier to delegate to. In Worker
|
|
201
|
+
* mode this classifier is still responsible for
|
|
202
|
+
* model loading and inference; the proxy just
|
|
203
|
+
* changes the thread on which it executes.
|
|
204
|
+
* @param browserConfig - Optional browser-side configuration. Controls
|
|
205
|
+
* whether Worker mode is attempted
|
|
206
|
+
* (`useWebWorker`, default `true`).
|
|
207
|
+
*/
|
|
208
|
+
constructor(
|
|
209
|
+
private readonly wrapped: IContentClassifier,
|
|
210
|
+
private readonly browserConfig?: BrowserConfig,
|
|
211
|
+
) {}
|
|
212
|
+
|
|
213
|
+
// -------------------------------------------------------------------------
|
|
214
|
+
// classify
|
|
215
|
+
// -------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Classify the provided text, routing to a Web Worker when available.
|
|
219
|
+
*
|
|
220
|
+
* ### Routing decision (evaluated once per call)
|
|
221
|
+
* 1. `typeof Worker === 'undefined'` → fallback (Node.js / no Worker API).
|
|
222
|
+
* 2. `browserConfig.useWebWorker === false` → fallback (explicit opt-out).
|
|
223
|
+
* 3. `workerFailed === true` → fallback (previous Worker creation error).
|
|
224
|
+
* 4. Otherwise → attempt to run in a Web Worker.
|
|
225
|
+
*
|
|
226
|
+
* If the Worker is created but fails to post a result within the
|
|
227
|
+
* classification request, the error is propagated as a rejected promise
|
|
228
|
+
* (not silently swallowed) so the orchestrator can log and fall back at
|
|
229
|
+
* a higher level.
|
|
230
|
+
*
|
|
231
|
+
* @param text - The text to classify. Must not be empty.
|
|
232
|
+
* @returns A promise that resolves with the classification result.
|
|
233
|
+
*/
|
|
234
|
+
async classify(text: string): Promise<ClassificationResult> {
|
|
235
|
+
// Determine whether to use a Web Worker.
|
|
236
|
+
const shouldUseWorker = this.shouldUseWebWorker();
|
|
237
|
+
|
|
238
|
+
if (!shouldUseWorker) {
|
|
239
|
+
// Fallback: delegate directly to the wrapped classifier on this thread.
|
|
240
|
+
return this.wrapped.classify(text);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Attempt to classify in a Worker.
|
|
244
|
+
return this.classifyInWorker(text);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// -------------------------------------------------------------------------
|
|
248
|
+
// dispose (optional IContentClassifier lifecycle hook)
|
|
249
|
+
// -------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Release resources held by the wrapped classifier.
|
|
253
|
+
*
|
|
254
|
+
* Delegates to `wrapped.dispose()` if it exists. Idempotent.
|
|
255
|
+
*/
|
|
256
|
+
async dispose(): Promise<void> {
|
|
257
|
+
if (this.wrapped.dispose) {
|
|
258
|
+
await this.wrapped.dispose();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// -------------------------------------------------------------------------
|
|
263
|
+
// Private helpers
|
|
264
|
+
// -------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Determine whether the current environment and configuration support
|
|
268
|
+
* running inference in a Web Worker.
|
|
269
|
+
*
|
|
270
|
+
* @returns `true` when Web Worker mode should be attempted.
|
|
271
|
+
*/
|
|
272
|
+
private shouldUseWebWorker(): boolean {
|
|
273
|
+
// Worker API is not available (Node.js, JSDOM without worker support, etc.)
|
|
274
|
+
if (typeof Worker === 'undefined') {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Caller explicitly opted out of Web Worker mode.
|
|
279
|
+
if (this.browserConfig?.useWebWorker === false) {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// A previous Worker creation attempt failed — stay on main thread.
|
|
284
|
+
if (this.workerFailed) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Run `classify(text)` inside a transient Web Worker.
|
|
293
|
+
*
|
|
294
|
+
* Each call creates a new Worker, sends a single `classify` message,
|
|
295
|
+
* awaits the `result` or `error` response, then terminates the Worker.
|
|
296
|
+
*
|
|
297
|
+
* If Worker creation itself throws (e.g. CSP violation), `workerFailed`
|
|
298
|
+
* is set to `true` and the call falls back to the wrapped classifier on
|
|
299
|
+
* the main thread.
|
|
300
|
+
*
|
|
301
|
+
* @param text - The text to classify inside the Worker.
|
|
302
|
+
* @returns A promise resolving with the {@link ClassificationResult}.
|
|
303
|
+
*/
|
|
304
|
+
private async classifyInWorker(text: string): Promise<ClassificationResult> {
|
|
305
|
+
let worker: Worker;
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
// Resolve the Worker script URL. We use the sibling classifier-worker
|
|
309
|
+
// module. In a bundled environment this will be a blob URL or a
|
|
310
|
+
// `new URL(...)` import; here we use a relative path that bundlers
|
|
311
|
+
// understand via the standard Worker constructor pattern.
|
|
312
|
+
worker = new Worker(new URL('../worker/classifier-worker.ts', import.meta.url), {
|
|
313
|
+
type: 'module',
|
|
314
|
+
});
|
|
315
|
+
} catch (err) {
|
|
316
|
+
// Worker could not be created (CSP, missing support, etc.).
|
|
317
|
+
// Mark as failed and fall back to the main thread.
|
|
318
|
+
this.workerFailed = true;
|
|
319
|
+
console.warn(
|
|
320
|
+
`[WorkerClassifierProxy] Worker creation failed for "${this.wrapped.id}"; ` +
|
|
321
|
+
`falling back to main-thread classification. Reason: ${err}`,
|
|
322
|
+
);
|
|
323
|
+
return this.wrapped.classify(text);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Build the request message.
|
|
327
|
+
const request: WorkerClassifyRequest = {
|
|
328
|
+
type: 'classify',
|
|
329
|
+
text,
|
|
330
|
+
modelId: this.wrapped.modelId,
|
|
331
|
+
// Default to non-quantized; the wrapped classifier's config owns this,
|
|
332
|
+
// but the Worker needs it to load the right model variant.
|
|
333
|
+
quantized: false,
|
|
334
|
+
taskType: 'text-classification',
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
return new Promise<ClassificationResult>((resolve, reject) => {
|
|
338
|
+
// Handle the single response message from the Worker.
|
|
339
|
+
worker.onmessage = (event: MessageEvent<WorkerResponse>) => {
|
|
340
|
+
const message = event.data;
|
|
341
|
+
|
|
342
|
+
if (message.type === 'result') {
|
|
343
|
+
resolve(message.result);
|
|
344
|
+
} else {
|
|
345
|
+
reject(new Error(`Worker classification error: ${message.error}`));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Terminate the Worker after receiving its response to free resources.
|
|
349
|
+
worker.terminate();
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// Handle any uncaught errors thrown inside the Worker.
|
|
353
|
+
worker.onerror = (errorEvent: ErrorEvent) => {
|
|
354
|
+
reject(
|
|
355
|
+
new Error(
|
|
356
|
+
`Worker runtime error in "${this.wrapped.id}": ${errorEvent.message}`,
|
|
357
|
+
),
|
|
358
|
+
);
|
|
359
|
+
worker.terminate();
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// Send the classify request to the Worker.
|
|
363
|
+
worker.postMessage(request);
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|