@intflows/genkit-guard 0.0.8 → 0.0.9

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/README.md CHANGED
@@ -1,4 +1,7 @@
1
- # **@intflows/genkit-guard**
1
+ # **@intflows/genkit-guard**
2
+
3
+ #### _This version uses OpenAI/privacy-filter instead of bert-base-NER_
4
+
2
5
  ### **Lightweight Intent, PII, and Safety Guardrails for Genkit**
3
6
 
4
7
  `@intflows/genkit-guard` provides a modular guardrail layer for Genkit flows.
@@ -24,7 +27,7 @@ This library is designed for developers who want **practical, production‑ready
24
27
  Blocks jailbreak attempts using pattern‑based heuristics.
25
28
 
26
29
  - **Model‑Light Architecture**
27
- The package uses local `all-MiniLM-L6-v2` and `bert-base-NER` Models, these Models are downloaded once and cached locally.
30
+ The package uses local `all-MiniLM-L6-v2` and `openai/privacy-filter` Models, these Models are downloaded once and cached locally.
28
31
 
29
32
  - **Drop‑in Genkit Middleware**
30
33
  Works with `ai.generate`, `ai.generateStream`, and Genkit flows.
@@ -38,12 +41,12 @@ This library is designed for developers who want **practical, production‑ready
38
41
  npm install @intflows/genkit-guard
39
42
  ```
40
43
 
41
- This library uses lightweight transformer models (MiniLM + BERT‑NER).
44
+ This library uses lightweight transformer models (MiniLM + Openai/privacy-filter).
42
45
 
43
46
  Download them once.
44
47
 
45
48
  ```bash
46
- ## Download the transformer models (MiniLM + BERT‑NER)
49
+ ## Download the transformer models (MiniLM + OpenAI/privacy-filter)
47
50
  node node_modules/@intflows/genkit-guard/scripts/download-model.js
48
51
  ```
49
52
 
@@ -62,6 +65,7 @@ npm install @intflows/genkit-guard
62
65
  # Download Local Models (Only needed once)
63
66
  node node_modules/@intflows/genkit-guard/scripts/download-model.js
64
67
  ```
68
+ _ This downloads the models to ./models folder, the total size is ~1.5 GB ( 1GB for Openai/privacy-filter + .5 GB for MiniLM-L6-v2)
65
69
 
66
70
  ### 2. Update genkit
67
71
 
@@ -133,9 +137,9 @@ Before the LLM sees the prompt:
133
137
  Detected PII includes:
134
138
 
135
139
  - Emails
136
- - Phone numbers
137
- - Names (NER)
140
+ - Phone numbers
138
141
  - AU identifiers (Medicare, TFN, ABN, etc.)
142
+ - PII detected by local Model (OpenAI/privacy-filter)
139
143
 
140
144
  ### **3. LLM Call**
141
145
  The masked prompt is sent to the model.
package/dist/index.d.ts CHANGED
@@ -3,4 +3,4 @@ export * from './core/types.js';
3
3
  /**
4
4
  * Pre-load the model to avoid cold-start delay on first user request.
5
5
  */
6
- export declare function initGuard(): Promise<void>;
6
+ export declare function initGuard(config?: any): Promise<void>;
package/dist/index.js CHANGED
@@ -5,11 +5,18 @@ export * from './core/types.js';
5
5
  /**
6
6
  * Pre-load the model to avoid cold-start delay on first user request.
7
7
  */
8
- export async function initGuard() {
8
+ export async function initGuard(config) {
9
9
  console.log('[Guard] Loading local models...');
10
- await Promise.all([
11
- ModelSingleton.getExtractor(),
12
- ModelSingleton.getNER()
13
- ]);
10
+ const extractorModel = config?.models?.extractor ?? 'Xenova/all-MiniLM-L6-v2';
11
+ const piiModel = config?.pii?.model;
12
+ const piiMode = config?.pii?.mode ?? 'ner';
13
+ const tasks = [ModelSingleton.getExtractor(extractorModel)];
14
+ if (piiMode === 'ner') {
15
+ tasks.push(ModelSingleton.getNER(piiModel ?? 'Xenova/bert-base-NER'));
16
+ }
17
+ else {
18
+ tasks.push(ModelSingleton.getPIIClassifier(piiModel ?? 'openai/privacy-filter'));
19
+ }
20
+ await Promise.all(tasks);
14
21
  console.log('[Guard] Models loaded');
15
22
  }
@@ -1 +1,300 @@
1
- export declare function guard(config: any): (req: any, next: any) => Promise<any>;
1
+ import { z } from 'genkit';
2
+ declare const guardConfigSchema: z.ZodObject<{
3
+ intent: z.ZodOptional<z.ZodObject<{
4
+ mode: z.ZodOptional<z.ZodString>;
5
+ allowedIntent: z.ZodOptional<z.ZodString>;
6
+ semantic: z.ZodObject<{
7
+ threshold: z.ZodOptional<z.ZodNumber>;
8
+ intents: z.ZodRecord<z.ZodString, z.ZodString>;
9
+ }, "strip", z.ZodTypeAny, {
10
+ intents: Record<string, string>;
11
+ threshold?: number | undefined;
12
+ }, {
13
+ intents: Record<string, string>;
14
+ threshold?: number | undefined;
15
+ }>;
16
+ }, "strip", z.ZodTypeAny, {
17
+ semantic: {
18
+ intents: Record<string, string>;
19
+ threshold?: number | undefined;
20
+ };
21
+ mode?: string | undefined;
22
+ allowedIntent?: string | undefined;
23
+ }, {
24
+ semantic: {
25
+ intents: Record<string, string>;
26
+ threshold?: number | undefined;
27
+ };
28
+ mode?: string | undefined;
29
+ allowedIntent?: string | undefined;
30
+ }>>;
31
+ pii: z.ZodOptional<z.ZodObject<{
32
+ reversible: z.ZodOptional<z.ZodBoolean>;
33
+ model: z.ZodOptional<z.ZodString>;
34
+ mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
35
+ }, "strip", z.ZodTypeAny, {
36
+ mode?: "ner" | "classifier" | undefined;
37
+ reversible?: boolean | undefined;
38
+ model?: string | undefined;
39
+ }, {
40
+ mode?: "ner" | "classifier" | undefined;
41
+ reversible?: boolean | undefined;
42
+ model?: string | undefined;
43
+ }>>;
44
+ models: z.ZodOptional<z.ZodObject<{
45
+ extractor: z.ZodOptional<z.ZodString>;
46
+ }, "strip", z.ZodTypeAny, {
47
+ extractor?: string | undefined;
48
+ }, {
49
+ extractor?: string | undefined;
50
+ }>>;
51
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
52
+ intent: z.ZodOptional<z.ZodObject<{
53
+ mode: z.ZodOptional<z.ZodString>;
54
+ allowedIntent: z.ZodOptional<z.ZodString>;
55
+ semantic: z.ZodObject<{
56
+ threshold: z.ZodOptional<z.ZodNumber>;
57
+ intents: z.ZodRecord<z.ZodString, z.ZodString>;
58
+ }, "strip", z.ZodTypeAny, {
59
+ intents: Record<string, string>;
60
+ threshold?: number | undefined;
61
+ }, {
62
+ intents: Record<string, string>;
63
+ threshold?: number | undefined;
64
+ }>;
65
+ }, "strip", z.ZodTypeAny, {
66
+ semantic: {
67
+ intents: Record<string, string>;
68
+ threshold?: number | undefined;
69
+ };
70
+ mode?: string | undefined;
71
+ allowedIntent?: string | undefined;
72
+ }, {
73
+ semantic: {
74
+ intents: Record<string, string>;
75
+ threshold?: number | undefined;
76
+ };
77
+ mode?: string | undefined;
78
+ allowedIntent?: string | undefined;
79
+ }>>;
80
+ pii: z.ZodOptional<z.ZodObject<{
81
+ reversible: z.ZodOptional<z.ZodBoolean>;
82
+ model: z.ZodOptional<z.ZodString>;
83
+ mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
84
+ }, "strip", z.ZodTypeAny, {
85
+ mode?: "ner" | "classifier" | undefined;
86
+ reversible?: boolean | undefined;
87
+ model?: string | undefined;
88
+ }, {
89
+ mode?: "ner" | "classifier" | undefined;
90
+ reversible?: boolean | undefined;
91
+ model?: string | undefined;
92
+ }>>;
93
+ models: z.ZodOptional<z.ZodObject<{
94
+ extractor: z.ZodOptional<z.ZodString>;
95
+ }, "strip", z.ZodTypeAny, {
96
+ extractor?: string | undefined;
97
+ }, {
98
+ extractor?: string | undefined;
99
+ }>>;
100
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
101
+ intent: z.ZodOptional<z.ZodObject<{
102
+ mode: z.ZodOptional<z.ZodString>;
103
+ allowedIntent: z.ZodOptional<z.ZodString>;
104
+ semantic: z.ZodObject<{
105
+ threshold: z.ZodOptional<z.ZodNumber>;
106
+ intents: z.ZodRecord<z.ZodString, z.ZodString>;
107
+ }, "strip", z.ZodTypeAny, {
108
+ intents: Record<string, string>;
109
+ threshold?: number | undefined;
110
+ }, {
111
+ intents: Record<string, string>;
112
+ threshold?: number | undefined;
113
+ }>;
114
+ }, "strip", z.ZodTypeAny, {
115
+ semantic: {
116
+ intents: Record<string, string>;
117
+ threshold?: number | undefined;
118
+ };
119
+ mode?: string | undefined;
120
+ allowedIntent?: string | undefined;
121
+ }, {
122
+ semantic: {
123
+ intents: Record<string, string>;
124
+ threshold?: number | undefined;
125
+ };
126
+ mode?: string | undefined;
127
+ allowedIntent?: string | undefined;
128
+ }>>;
129
+ pii: z.ZodOptional<z.ZodObject<{
130
+ reversible: z.ZodOptional<z.ZodBoolean>;
131
+ model: z.ZodOptional<z.ZodString>;
132
+ mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
133
+ }, "strip", z.ZodTypeAny, {
134
+ mode?: "ner" | "classifier" | undefined;
135
+ reversible?: boolean | undefined;
136
+ model?: string | undefined;
137
+ }, {
138
+ mode?: "ner" | "classifier" | undefined;
139
+ reversible?: boolean | undefined;
140
+ model?: string | undefined;
141
+ }>>;
142
+ models: z.ZodOptional<z.ZodObject<{
143
+ extractor: z.ZodOptional<z.ZodString>;
144
+ }, "strip", z.ZodTypeAny, {
145
+ extractor?: string | undefined;
146
+ }, {
147
+ extractor?: string | undefined;
148
+ }>>;
149
+ }, z.ZodTypeAny, "passthrough">>;
150
+ export declare const guardMiddleware: import("genkit").GenerateMiddleware<z.ZodObject<{
151
+ intent: z.ZodOptional<z.ZodObject<{
152
+ mode: z.ZodOptional<z.ZodString>;
153
+ allowedIntent: z.ZodOptional<z.ZodString>;
154
+ semantic: z.ZodObject<{
155
+ threshold: z.ZodOptional<z.ZodNumber>;
156
+ intents: z.ZodRecord<z.ZodString, z.ZodString>;
157
+ }, "strip", z.ZodTypeAny, {
158
+ intents: Record<string, string>;
159
+ threshold?: number | undefined;
160
+ }, {
161
+ intents: Record<string, string>;
162
+ threshold?: number | undefined;
163
+ }>;
164
+ }, "strip", z.ZodTypeAny, {
165
+ semantic: {
166
+ intents: Record<string, string>;
167
+ threshold?: number | undefined;
168
+ };
169
+ mode?: string | undefined;
170
+ allowedIntent?: string | undefined;
171
+ }, {
172
+ semantic: {
173
+ intents: Record<string, string>;
174
+ threshold?: number | undefined;
175
+ };
176
+ mode?: string | undefined;
177
+ allowedIntent?: string | undefined;
178
+ }>>;
179
+ pii: z.ZodOptional<z.ZodObject<{
180
+ reversible: z.ZodOptional<z.ZodBoolean>;
181
+ model: z.ZodOptional<z.ZodString>;
182
+ mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
183
+ }, "strip", z.ZodTypeAny, {
184
+ mode?: "ner" | "classifier" | undefined;
185
+ reversible?: boolean | undefined;
186
+ model?: string | undefined;
187
+ }, {
188
+ mode?: "ner" | "classifier" | undefined;
189
+ reversible?: boolean | undefined;
190
+ model?: string | undefined;
191
+ }>>;
192
+ models: z.ZodOptional<z.ZodObject<{
193
+ extractor: z.ZodOptional<z.ZodString>;
194
+ }, "strip", z.ZodTypeAny, {
195
+ extractor?: string | undefined;
196
+ }, {
197
+ extractor?: string | undefined;
198
+ }>>;
199
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
200
+ intent: z.ZodOptional<z.ZodObject<{
201
+ mode: z.ZodOptional<z.ZodString>;
202
+ allowedIntent: z.ZodOptional<z.ZodString>;
203
+ semantic: z.ZodObject<{
204
+ threshold: z.ZodOptional<z.ZodNumber>;
205
+ intents: z.ZodRecord<z.ZodString, z.ZodString>;
206
+ }, "strip", z.ZodTypeAny, {
207
+ intents: Record<string, string>;
208
+ threshold?: number | undefined;
209
+ }, {
210
+ intents: Record<string, string>;
211
+ threshold?: number | undefined;
212
+ }>;
213
+ }, "strip", z.ZodTypeAny, {
214
+ semantic: {
215
+ intents: Record<string, string>;
216
+ threshold?: number | undefined;
217
+ };
218
+ mode?: string | undefined;
219
+ allowedIntent?: string | undefined;
220
+ }, {
221
+ semantic: {
222
+ intents: Record<string, string>;
223
+ threshold?: number | undefined;
224
+ };
225
+ mode?: string | undefined;
226
+ allowedIntent?: string | undefined;
227
+ }>>;
228
+ pii: z.ZodOptional<z.ZodObject<{
229
+ reversible: z.ZodOptional<z.ZodBoolean>;
230
+ model: z.ZodOptional<z.ZodString>;
231
+ mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
232
+ }, "strip", z.ZodTypeAny, {
233
+ mode?: "ner" | "classifier" | undefined;
234
+ reversible?: boolean | undefined;
235
+ model?: string | undefined;
236
+ }, {
237
+ mode?: "ner" | "classifier" | undefined;
238
+ reversible?: boolean | undefined;
239
+ model?: string | undefined;
240
+ }>>;
241
+ models: z.ZodOptional<z.ZodObject<{
242
+ extractor: z.ZodOptional<z.ZodString>;
243
+ }, "strip", z.ZodTypeAny, {
244
+ extractor?: string | undefined;
245
+ }, {
246
+ extractor?: string | undefined;
247
+ }>>;
248
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
249
+ intent: z.ZodOptional<z.ZodObject<{
250
+ mode: z.ZodOptional<z.ZodString>;
251
+ allowedIntent: z.ZodOptional<z.ZodString>;
252
+ semantic: z.ZodObject<{
253
+ threshold: z.ZodOptional<z.ZodNumber>;
254
+ intents: z.ZodRecord<z.ZodString, z.ZodString>;
255
+ }, "strip", z.ZodTypeAny, {
256
+ intents: Record<string, string>;
257
+ threshold?: number | undefined;
258
+ }, {
259
+ intents: Record<string, string>;
260
+ threshold?: number | undefined;
261
+ }>;
262
+ }, "strip", z.ZodTypeAny, {
263
+ semantic: {
264
+ intents: Record<string, string>;
265
+ threshold?: number | undefined;
266
+ };
267
+ mode?: string | undefined;
268
+ allowedIntent?: string | undefined;
269
+ }, {
270
+ semantic: {
271
+ intents: Record<string, string>;
272
+ threshold?: number | undefined;
273
+ };
274
+ mode?: string | undefined;
275
+ allowedIntent?: string | undefined;
276
+ }>>;
277
+ pii: z.ZodOptional<z.ZodObject<{
278
+ reversible: z.ZodOptional<z.ZodBoolean>;
279
+ model: z.ZodOptional<z.ZodString>;
280
+ mode: z.ZodOptional<z.ZodEnum<["ner", "classifier"]>>;
281
+ }, "strip", z.ZodTypeAny, {
282
+ mode?: "ner" | "classifier" | undefined;
283
+ reversible?: boolean | undefined;
284
+ model?: string | undefined;
285
+ }, {
286
+ mode?: "ner" | "classifier" | undefined;
287
+ reversible?: boolean | undefined;
288
+ model?: string | undefined;
289
+ }>>;
290
+ models: z.ZodOptional<z.ZodObject<{
291
+ extractor: z.ZodOptional<z.ZodString>;
292
+ }, "strip", z.ZodTypeAny, {
293
+ extractor?: string | undefined;
294
+ }, {
295
+ extractor?: string | undefined;
296
+ }>>;
297
+ }, z.ZodTypeAny, "passthrough">>, void>;
298
+ export declare const guardPlugin: (pluginOptions: void) => import("@genkit-ai/ai").GenkitPluginV2;
299
+ export declare function guard(config?: z.infer<typeof guardConfigSchema>): (req: any, ctxOrNext: any, maybeNext?: any) => Promise<any>;
300
+ export {};
@@ -1,113 +1,159 @@
1
+ import { generateMiddleware, z } from 'genkit';
1
2
  import { analyzeIntentStructured, detectInjection } from '../intent/intentAnalyzer.js';
2
3
  import { detectPII } from '../pii/detector.js';
3
4
  import { PiiTokenizer } from '../pii/tokenizer.js';
5
+ const guardConfigSchema = z.object({
6
+ intent: z.object({
7
+ mode: z.string().optional(),
8
+ allowedIntent: z.string().optional(),
9
+ semantic: z.object({
10
+ threshold: z.number().optional(),
11
+ intents: z.record(z.string(), z.string()),
12
+ }),
13
+ }).optional(),
14
+ pii: z.object({
15
+ reversible: z.boolean().optional(),
16
+ model: z.string().optional(),
17
+ mode: z.enum(['ner', 'classifier']).optional(),
18
+ }).optional(),
19
+ models: z.object({
20
+ extractor: z.string().optional(),
21
+ }).optional(),
22
+ }).passthrough();
23
+ export const guardMiddleware = generateMiddleware({
24
+ name: 'genkitGuard',
25
+ description: 'Blocks prompt injection and disallowed intent, then masks PII before model calls and unmasks model responses.',
26
+ configSchema: guardConfigSchema,
27
+ }, ({ config }) => createGuardHooks(config));
28
+ export const guardPlugin = guardMiddleware.plugin;
4
29
  export function guard(config) {
5
- return async (req, next) => {
6
- const input = req.prompt ||
7
- req.messages?.[req.messages.length - 1]?.content?.[0]?.text ||
8
- "";
9
- // -------------------------
10
- // 1. INTENT ANALYSIS
11
- // -------------------------
12
- const isInjection = await detectInjection(input);
13
- if (isInjection) {
14
- console.warn(`[Intent Guard] Prompt injection pattern detected in input`);
15
- return block("Prompt injection detected", {
16
- reason: "pattern_match"
17
- });
30
+ const hooks = createGuardHooks(config);
31
+ const baseMiddleware = guardMiddleware(config);
32
+ // 1. Create the wrapper function runner
33
+ const fnRunner = async (req, ctxOrNext, maybeNext) => {
34
+ if (typeof maybeNext === 'function') {
35
+ return hooks.model(req, ctxOrNext, maybeNext);
18
36
  }
19
- console.log(`[Intent Guard] Analyzing intent for user's input`);
20
- const intentResult = await analyzeIntentStructured(input, config.intent.semantic.intents, config.intent.semantic.threshold);
21
- console.log(`[Intent Guard] Detected intent: ${intentResult.intent} (score: ${intentResult.score.toFixed(2)})`);
22
- if (!intentResult.allowed) {
23
- console.warn(`[Intent Guard] Intent "${intentResult.intent}" not allowed ${intentResult.allowed}`);
24
- return block("Intent not allowed", {
25
- intent: intentResult.intent,
26
- score: intentResult.score
37
+ return hooks.model(req, {}, async (modifiedReq) => ctxOrNext(modifiedReq || req));
38
+ };
39
+ // 2. Combine the base middleware properties and custom hooks into a source object
40
+ const source = Object.assign({}, baseMiddleware, hooks);
41
+ // 3. Safely copy properties onto the function runner, explicitly skipping the read-only 'name' property
42
+ for (const key of Object.keys(source)) {
43
+ if (key === 'name')
44
+ continue; // Prevent the TypeError
45
+ // Use defineProperty or simple assignment for everything else
46
+ Object.defineProperty(fnRunner, key, {
47
+ value: source[key],
48
+ writable: true,
49
+ configurable: true,
50
+ enumerable: true
51
+ });
52
+ }
53
+ return fnRunner;
54
+ }
55
+ function createGuardHooks(config) {
56
+ return {
57
+ model: async (req, ctx, next) => {
58
+ const input = getInputText(req);
59
+ const isInjection = await detectInjection(input);
60
+ if (isInjection) {
61
+ console.warn('[Intent Guard] Prompt injection pattern detected in input');
62
+ return block('Prompt injection detected', {
63
+ reason: 'pattern_match',
64
+ });
65
+ }
66
+ console.log("[Intent Guard] Analyzing intent for user's input");
67
+ const intentResult = await analyzeIntentStructured(input, config?.intent?.semantic?.intents ?? {}, config?.intent?.semantic?.threshold ?? 0.7);
68
+ console.log(`[Intent Guard] Detected intent: ${intentResult.intent} (score: ${intentResult.score.toFixed(2)})`);
69
+ if (!intentResult.allowed) {
70
+ console.warn(`[Intent Guard] Intent "${intentResult.intent}" not allowed ${intentResult.allowed}`);
71
+ return block('Intent not allowed', {
72
+ intent: intentResult.intent,
73
+ score: intentResult.score,
74
+ });
75
+ }
76
+ const piiResponse = await detectPII(input, {
77
+ model: config?.pii?.model,
78
+ mode: config?.pii?.mode,
27
79
  });
28
- }
29
- // -------------------------
30
- // 2. PII DETECTION + MASKING
31
- // -------------------------
32
- const piiMatches = await detectPII(input);
33
- console.log(`[PII Guard] Detected PII: ${piiMatches.length} matches found`);
34
- const tokenizer = new PiiTokenizer(); // <-- SINGLE INSTANCE
35
- const piiResult = tokenizer.mask(input, piiMatches);
36
- console.log(`[PII Guard] Masked PII: ${piiResult.piiTypes.length} types found`);
37
- // Attach tokenizer so response can unmask
38
- req.metadata = {
39
- ...req.metadata,
40
- piiTokenizer: tokenizer,
41
- intent: intentResult.intent,
42
- score: intentResult.score,
43
- piiDetected: piiMatches.length > 0,
44
- piiTypes: piiResult.piiTypes,
45
- maskedInput: piiResult.maskedText
46
- };
47
- // Replace input
48
- req.prompt = piiResult.maskedText;
49
- req.messages = [
50
- {
51
- role: 'user',
52
- content: [{ text: piiResult.maskedText }],
53
- },
54
- ];
55
- // -------------------------
56
- // 3. LLM CALL
57
- // -------------------------
58
- const res = await next(req);
59
- // -------------------------
60
- // 4. RESPONSE UNMASK
61
- // -------------------------
62
- console.log(`[PII Guard] Unmasking response if needed`);
63
- if (tokenizer) {
64
- /**
65
- * RECURSIVE TRANSFORMER
66
- * This will find every string in the Genkit response (no matter if it's in
67
- * candidates, message, custom, or output) and unmask it.
68
- */
69
- const transform = (obj) => {
70
- // 1. If it's a string, unmask it
71
- if (typeof obj === 'string') {
72
- return tokenizer.unmask(obj);
73
- }
74
- // 2. If it's an array, transform each element
75
- if (Array.isArray(obj)) {
76
- return obj.map(transform);
77
- }
78
- // 3. If it's an object, transform each value
79
- if (obj !== null && typeof obj === 'object') {
80
- // Note: We iterate keys and mutate the object directly
81
- // to ensure Genkit's internal references are updated.
82
- for (const key of Object.keys(obj)) {
83
- obj[key] = transform(obj[key]);
84
- }
85
- return obj;
86
- }
87
- // 4. Return as-is for numbers/booleans/null
88
- return obj;
80
+ const piiMatches = piiResponse?.matches || [];
81
+ console.log(`[PII Guard] Detected PII: ${piiMatches.length} matches found` + (piiResponse.classifier ? ' (classifier output present)' : ''));
82
+ const tokenizer = new PiiTokenizer();
83
+ const piiResult = tokenizer.mask(input, piiMatches);
84
+ console.log(`[PII Guard] Masked PII: ${piiResult.piiTypes.length} types found`);
85
+ req.metadata = {
86
+ ...req.metadata,
87
+ piiTokenizer: tokenizer,
88
+ intent: intentResult.intent,
89
+ score: intentResult.score,
90
+ piiDetected: piiMatches.length > 0,
91
+ piiTypes: piiResult.piiTypes,
92
+ maskedInput: piiResult.maskedText,
93
+ piiModel: config?.pii?.model,
94
+ piiMode: config?.pii?.mode,
95
+ piiClassifierOutput: piiResponse.classifier,
89
96
  };
90
- // ----------------------------------------------------------------------
91
- // 5. Transform the entire response object in-place to unmask all strings
92
- // ----------------------------------------------------------------------
93
- transform(res);
94
- console.log("[PII Guard] Deep unmasking complete across all candidates and custom fields.");
97
+ replaceInputText(req, piiResult.maskedText);
98
+ const res = await next(req, ctx);
99
+ console.log('[PII Guard] Unmasking response if needed');
100
+ unmaskResponse(res, tokenizer);
101
+ console.log('[PII Guard] Deep unmasking complete across all candidates and custom fields.');
102
+ return res;
103
+ },
104
+ };
105
+ }
106
+ function getInputText(req) {
107
+ if (typeof req.prompt === 'string') {
108
+ return req.prompt;
109
+ }
110
+ const lastMessage = req.messages?.[req.messages.length - 1];
111
+ const firstContent = lastMessage?.content?.[0];
112
+ if (typeof firstContent?.text === 'string') {
113
+ return firstContent.text;
114
+ }
115
+ if (typeof firstContent === 'string') {
116
+ return firstContent;
117
+ }
118
+ return '';
119
+ }
120
+ function replaceInputText(req, text) {
121
+ if (typeof req.prompt === 'string') {
122
+ req.prompt = text;
123
+ }
124
+ req.messages = [
125
+ {
126
+ role: 'user',
127
+ content: [{ text }],
128
+ },
129
+ ];
130
+ }
131
+ function unmaskResponse(res, tokenizer) {
132
+ const transform = (obj) => {
133
+ if (typeof obj === 'string') {
134
+ return tokenizer.unmask(obj);
135
+ }
136
+ if (Array.isArray(obj)) {
137
+ return obj.map(transform);
138
+ }
139
+ if (obj !== null && typeof obj === 'object') {
140
+ for (const key of Object.keys(obj)) {
141
+ obj[key] = transform(obj[key]);
142
+ }
143
+ return obj;
95
144
  }
96
- // ---------------------------------------------------------
97
- // 6. Return the modified response with unmasked content
98
- // ---------------------------------------------------------
99
- return res;
145
+ return obj;
100
146
  };
147
+ transform(res);
101
148
  }
102
- // Helper to create a blocked response
103
149
  function block(message, metadata) {
104
150
  return {
105
151
  finishReason: 'blocked',
106
152
  output: {
107
- type: "error",
108
- status: "BLOCKED",
109
- message
153
+ type: 'error',
154
+ status: 'BLOCKED',
155
+ message,
110
156
  },
111
- metadata
157
+ metadata,
112
158
  };
113
159
  }
@@ -1,4 +1,10 @@
1
- export declare function detectPII(text: string): Promise<{
2
- type: string;
3
- value: string;
4
- }[]>;
1
+ export declare function detectPII(text: string, opts?: {
2
+ model?: string;
3
+ mode?: 'ner' | 'classifier';
4
+ }): Promise<{
5
+ matches: {
6
+ type: string;
7
+ value: string;
8
+ }[];
9
+ classifier: any;
10
+ }>;
@@ -15,20 +15,33 @@ const REGEX_RULES = [
15
15
  // CREDIT CARD (keep your existing one if needed)
16
16
  { type: 'CREDIT_CARD', pattern: /\b(?:\d[ -]*?){13,16}\b/g }
17
17
  ];
18
- export async function detectPII(text) {
19
- const ner = await ModelSingleton.getNER();
18
+ export async function detectPII(text, opts) {
19
+ const mode = opts?.mode ?? 'ner';
20
+ const model = opts?.model;
20
21
  const results = [];
21
- // ---- NER ----
22
- const entities = await ner(text);
23
- for (const e of entities) {
24
- if (e.entity.includes('PER')) {
25
- results.push({ type: 'NAME', value: e.word.replace('##', '') });
26
- }
27
- }
28
- // ---- REGEX ----
22
+ // ---- REGEX (always run) ----
29
23
  for (const rule of REGEX_RULES) {
30
24
  const matches = text.match(rule.pattern) || [];
31
25
  matches.forEach(m => results.push({ type: rule.type, value: m }));
32
26
  }
33
- return results;
27
+ // ---- NER ----
28
+ let classifierOutput = undefined;
29
+ if (mode === 'ner') {
30
+ const ner = await ModelSingleton.getNER(model);
31
+ const entities = await ner(text);
32
+ for (const e of entities) {
33
+ if (e.entity && e.entity.includes('PER')) {
34
+ results.push({ type: 'NAME', value: (e.word || '').replace(/##/g, '') });
35
+ }
36
+ }
37
+ }
38
+ else {
39
+ // classifier mode: we call the classifier and return its output alongside regex matches.
40
+ const cls = await ModelSingleton.getPIIClassifier(model);
41
+ classifierOutput = await cls(text);
42
+ }
43
+ return {
44
+ matches: results,
45
+ classifier: classifierOutput
46
+ };
34
47
  }
@@ -1,7 +1,14 @@
1
1
  export declare class ModelSingleton {
2
- private static extractor;
3
- private static classifier;
2
+ private static extractors;
3
+ private static nerClassifiers;
4
+ private static textClassifiers;
4
5
  static init(): void;
5
- static getExtractor(): Promise<any>;
6
- static getNER(): Promise<any>;
6
+ static getExtractor(modelName?: string): Promise<any>;
7
+ static getNER(modelName?: string): Promise<any>;
8
+ static getPIIClassifier(modelName?: string): Promise<any>;
9
+ static preload(models?: {
10
+ extractor?: string;
11
+ ner?: string;
12
+ pii?: string;
13
+ }): Promise<void>;
7
14
  }
@@ -6,8 +6,9 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
6
  env.allowRemoteModels = false;
7
7
  env.localModelPath = path.join(__dirname, '../../models');
8
8
  export class ModelSingleton {
9
- static extractor = null;
10
- static classifier = null;
9
+ static extractors = new Map();
10
+ static nerClassifiers = new Map();
11
+ static textClassifiers = new Map();
11
12
  static init() {
12
13
  // Always resolve model path relative to the client app, not the library
13
14
  const projectRoot = process.cwd();
@@ -22,19 +23,38 @@ export class ModelSingleton {
22
23
  env.allowRemoteModels = true;
23
24
  console.log("[Guard] Using model directory:", modelPath);
24
25
  }
25
- static async getExtractor() {
26
- if (!this.extractor) {
26
+ static async getExtractor(modelName = 'Xenova/all-MiniLM-L6-v2') {
27
+ if (!this.extractors.has(modelName)) {
27
28
  this.init();
28
- this.extractor = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
29
+ const inst = await pipeline('feature-extraction', modelName);
30
+ this.extractors.set(modelName, inst);
29
31
  }
30
- return this.extractor;
32
+ return this.extractors.get(modelName);
31
33
  }
32
- static async getNER() {
33
- if (!this.classifier) {
34
+ static async getNER(modelName = 'Xenova/bert-base-NER') {
35
+ if (!this.nerClassifiers.has(modelName)) {
34
36
  this.init();
35
- // Token classification model for identifying sensitive entities
36
- this.classifier = await pipeline('token-classification', 'Xenova/bert-base-NER');
37
+ const inst = await pipeline('token-classification', modelName);
38
+ this.nerClassifiers.set(modelName, inst);
37
39
  }
38
- return this.classifier;
40
+ return this.nerClassifiers.get(modelName);
41
+ }
42
+ static async getPIIClassifier(modelName = 'openai/privacy-filter') {
43
+ if (!this.textClassifiers.has(modelName)) {
44
+ this.init();
45
+ const inst = await pipeline('text-classification', modelName);
46
+ this.textClassifiers.set(modelName, inst);
47
+ }
48
+ return this.textClassifiers.get(modelName);
49
+ }
50
+ static async preload(models) {
51
+ const tasks = [];
52
+ if (models?.extractor)
53
+ tasks.push(this.getExtractor(models.extractor));
54
+ if (models?.ner)
55
+ tasks.push(this.getNER(models.ner));
56
+ if (models?.pii)
57
+ tasks.push(this.getPIIClassifier(models.pii));
58
+ await Promise.all(tasks);
39
59
  }
40
60
  }
package/package.json CHANGED
@@ -4,6 +4,10 @@
4
4
  "maintainers": [
5
5
  "Hemant Kohli"
6
6
  ],
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/IntFlows/genkit-guard"
10
+ },
7
11
  "keywords": [
8
12
  "genkit",
9
13
  "genkit-guard",
@@ -18,7 +22,7 @@
18
22
  "huggingface"
19
23
  ],
20
24
  "license": "Apache-2.0",
21
- "version": "0.0.8",
25
+ "version": "0.0.9",
22
26
  "type": "module",
23
27
  "exports": "./dist/index.js",
24
28
  "types": "./dist/index.d.ts",
@@ -33,15 +37,16 @@
33
37
  "build": "tsc",
34
38
  "prepublishOnly": "npm run build"
35
39
  },
36
- "peerDependencies": {
37
- "genkit": ">=0.5.0"
38
- },
39
40
  "dependencies": {
40
- "@huggingface/transformers": "^3.0.0",
41
- "zod": "^3.23.0"
41
+ "@huggingface/transformers": "^4.2.0",
42
+ "zod": "^4.4.3"
43
+ },
44
+ "peerDependencies": {
45
+ "genkit": "^1.37.0"
42
46
  },
43
47
  "devDependencies": {
44
- "typescript": "^5.6.0",
45
- "@types/node": "^20.0.0"
48
+ "@types/node": "^25.8.0",
49
+ "genkit": "^1.37.0",
50
+ "typescript": "^6.0.3"
46
51
  }
47
52
  }
@@ -13,9 +13,14 @@ async function download() {
13
13
  await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', {
14
14
  device: 'cpu'
15
15
  });
16
- console.log('Downloading BERT-NER to ./models...');
17
- await pipeline('token-classification', 'Xenova/bert-base-NER', {
18
- device: 'cpu'
16
+ //console.log('Downloading BERT-NER to ./models...');
17
+ // await pipeline('token-classification', 'Xenova/bert-base-NER', {
18
+ // device: 'cpu'
19
+ // });
20
+
21
+ console.log('Downloading openai/privacy-filter to ./models...');
22
+ await pipeline('token-classification', 'openai/privacy-filter', {
23
+ device: 'cpu', dtype: "q4"
19
24
  });
20
25
  console.log('Model downloaded successfully.');
21
26
  }