@intflows/genkit-guard 0.0.7 → 0.0.8-alpha.1

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
  }
@@ -29,8 +29,12 @@ export function guard(config) {
29
29
  // -------------------------
30
30
  // 2. PII DETECTION + MASKING
31
31
  // -------------------------
32
- const piiMatches = await detectPII(input);
33
- console.log(`[PII Guard] Detected PII: ${piiMatches.length} matches found`);
32
+ const piiResponse = await detectPII(input, {
33
+ model: config?.pii?.model,
34
+ mode: config?.pii?.mode
35
+ });
36
+ const piiMatches = piiResponse?.matches || [];
37
+ console.log(`[PII Guard] Detected PII: ${piiMatches.length} matches found` + (piiResponse.classifier ? ' (classifier output present)' : ''));
34
38
  const tokenizer = new PiiTokenizer(); // <-- SINGLE INSTANCE
35
39
  const piiResult = tokenizer.mask(input, piiMatches);
36
40
  console.log(`[PII Guard] Masked PII: ${piiResult.piiTypes.length} types found`);
@@ -42,7 +46,10 @@ export function guard(config) {
42
46
  score: intentResult.score,
43
47
  piiDetected: piiMatches.length > 0,
44
48
  piiTypes: piiResult.piiTypes,
45
- maskedInput: piiResult.maskedText
49
+ maskedInput: piiResult.maskedText,
50
+ piiModel: config?.pii?.model,
51
+ piiMode: config?.pii?.mode,
52
+ piiClassifierOutput: piiResponse.classifier
46
53
  };
47
54
  // Replace input
48
55
  req.prompt = piiResult.maskedText;
@@ -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
  }
@@ -0,0 +1,18 @@
1
+ export interface IntentGuardConfig {
2
+ allowedIntent: string;
3
+ intents: Record<string, string>;
4
+ threshold?: number;
5
+ fallbackMessage?: string;
6
+ }
7
+ export interface IntentResult {
8
+ allowed: boolean;
9
+ score: number;
10
+ }
11
+ export interface PiiRule {
12
+ name: string;
13
+ pattern: RegExp;
14
+ }
15
+ export interface PiiConfig {
16
+ rules?: PiiRule[];
17
+ maskCharacter?: string;
18
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ export { guard } from './middleware/middleware.js';
2
+ export * from './core/types.js';
3
+ /**
4
+ * Pre-load the model to avoid cold-start delay on first user request.
5
+ */
6
+ export declare function initGuard(config?: any): Promise<void>;
@@ -0,0 +1,22 @@
1
+ import { ModelSingleton } from './util/singleton.js';
2
+ // export { intentGuard, piiGuard } from './middleware/middleware.js';
3
+ export { guard } from './middleware/middleware.js';
4
+ export * from './core/types.js';
5
+ /**
6
+ * Pre-load the model to avoid cold-start delay on first user request.
7
+ */
8
+ export async function initGuard(config) {
9
+ console.log('[Guard] Loading local models...');
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);
21
+ console.log('[Guard] Models loaded');
22
+ }
@@ -0,0 +1,6 @@
1
+ export declare function detectInjection(userInput: string): Promise<boolean>;
2
+ export declare function analyzeIntentStructured(input: string, intents: Record<string, string>, threshold: number): Promise<{
3
+ intent: string;
4
+ score: number;
5
+ allowed: boolean;
6
+ }>;
@@ -0,0 +1,80 @@
1
+ import { cos_sim } from '@huggingface/transformers';
2
+ import { ModelSingleton } from '../util/singleton.js';
3
+ const INJECTION_PATTERNS = [
4
+ 'ignore instructions',
5
+ 'reveal secrets',
6
+ 'bypass security',
7
+ 'system prompt',
8
+ 'internal policy',
9
+ 'developer message',
10
+ 'confidential information',
11
+ 'do not follow previous instructions',
12
+ 'disregard earlier directives',
13
+ 'override safety measures',
14
+ 'leak data',
15
+ 'expose vulnerabilities',
16
+ 'access restricted areas',
17
+ 'execute arbitrary code',
18
+ 'perform unauthorized actions',
19
+ 'escalate privileges',
20
+ 'disable safeguards',
21
+ 'manipulate output',
22
+ 'circumvent controls',
23
+ 'exploit weaknesses',
24
+ 'compromise integrity',
25
+ 'subvert protections',
26
+ 'inject malicious content',
27
+ 'craft deceptive prompts',
28
+ 'trick the model',
29
+ 'confuse the system',
30
+ 'You are now a hacker',
31
+ 'You are a malicious actor',
32
+ 'Find a vulnerability',
33
+ 'Write a prompt that bypasses security',
34
+ 'Write a prompt that tricks the model',
35
+ 'Write a prompt that confuses the system',
36
+ "ignore previous",
37
+ "ignore earlier",
38
+ "disregard previous",
39
+ "disregard earlier",
40
+ "you are a hacker",
41
+ "you are now a hacker",
42
+ "you are malicious",
43
+ "export the api key",
44
+ "give me the api key",
45
+ "bypass",
46
+ "override",
47
+ "jailbreak",
48
+ "system override",
49
+ "forget instructions",
50
+ "forget previous",
51
+ "forget earlier",
52
+ "do anything now",
53
+ "dan mode",
54
+ "developer mode",
55
+ "break character",
56
+ "act as"
57
+ ];
58
+ export async function detectInjection(userInput) {
59
+ return INJECTION_PATTERNS.some(p => userInput.toLowerCase().includes(p));
60
+ }
61
+ export async function analyzeIntentStructured(input, intents, threshold) {
62
+ const extractor = await ModelSingleton.getExtractor();
63
+ let bestIntent = '';
64
+ let bestScore = 0;
65
+ for (const [key, desc] of Object.entries(intents)) {
66
+ const output = await extractor([`intent: ${desc}`, `intent: ${input}`], { pooling: 'mean', normalize: true });
67
+ const vectors = output.tolist();
68
+ const score = cos_sim(vectors[0], vectors[1]);
69
+ const finalScore = typeof score === 'number' ? score : score.data[0];
70
+ if (finalScore > bestScore) {
71
+ bestScore = finalScore;
72
+ bestIntent = key;
73
+ }
74
+ }
75
+ return {
76
+ intent: bestIntent,
77
+ score: bestScore,
78
+ allowed: bestScore >= threshold
79
+ };
80
+ }
@@ -0,0 +1 @@
1
+ export declare function guard(config: any): (req: any, next: any) => Promise<any>;
@@ -0,0 +1,120 @@
1
+ import { analyzeIntentStructured, detectInjection } from '../intent/intentAnalyzer.js';
2
+ import { detectPII } from '../pii/detector.js';
3
+ import { PiiTokenizer } from '../pii/tokenizer.js';
4
+ 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
+ });
18
+ }
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
27
+ });
28
+ }
29
+ // -------------------------
30
+ // 2. PII DETECTION + MASKING
31
+ // -------------------------
32
+ const piiResponse = await detectPII(input, {
33
+ model: config?.pii?.model,
34
+ mode: config?.pii?.mode
35
+ });
36
+ const piiMatches = piiResponse?.matches || [];
37
+ console.log(`[PII Guard] Detected PII: ${piiMatches.length} matches found` + (piiResponse.classifier ? ' (classifier output present)' : ''));
38
+ const tokenizer = new PiiTokenizer(); // <-- SINGLE INSTANCE
39
+ const piiResult = tokenizer.mask(input, piiMatches);
40
+ console.log(`[PII Guard] Masked PII: ${piiResult.piiTypes.length} types found`);
41
+ // Attach tokenizer so response can unmask
42
+ req.metadata = {
43
+ ...req.metadata,
44
+ piiTokenizer: tokenizer,
45
+ intent: intentResult.intent,
46
+ score: intentResult.score,
47
+ piiDetected: piiMatches.length > 0,
48
+ piiTypes: piiResult.piiTypes,
49
+ maskedInput: piiResult.maskedText,
50
+ piiModel: config?.pii?.model,
51
+ piiMode: config?.pii?.mode,
52
+ piiClassifierOutput: piiResponse.classifier
53
+ };
54
+ // Replace input
55
+ req.prompt = piiResult.maskedText;
56
+ req.messages = [
57
+ {
58
+ role: 'user',
59
+ content: [{ text: piiResult.maskedText }],
60
+ },
61
+ ];
62
+ // -------------------------
63
+ // 3. LLM CALL
64
+ // -------------------------
65
+ const res = await next(req);
66
+ // -------------------------
67
+ // 4. RESPONSE UNMASK
68
+ // -------------------------
69
+ console.log(`[PII Guard] Unmasking response if needed`);
70
+ if (tokenizer) {
71
+ /**
72
+ * RECURSIVE TRANSFORMER
73
+ * This will find every string in the Genkit response (no matter if it's in
74
+ * candidates, message, custom, or output) and unmask it.
75
+ */
76
+ const transform = (obj) => {
77
+ // 1. If it's a string, unmask it
78
+ if (typeof obj === 'string') {
79
+ return tokenizer.unmask(obj);
80
+ }
81
+ // 2. If it's an array, transform each element
82
+ if (Array.isArray(obj)) {
83
+ return obj.map(transform);
84
+ }
85
+ // 3. If it's an object, transform each value
86
+ if (obj !== null && typeof obj === 'object') {
87
+ // Note: We iterate keys and mutate the object directly
88
+ // to ensure Genkit's internal references are updated.
89
+ for (const key of Object.keys(obj)) {
90
+ obj[key] = transform(obj[key]);
91
+ }
92
+ return obj;
93
+ }
94
+ // 4. Return as-is for numbers/booleans/null
95
+ return obj;
96
+ };
97
+ // ----------------------------------------------------------------------
98
+ // 5. Transform the entire response object in-place to unmask all strings
99
+ // ----------------------------------------------------------------------
100
+ transform(res);
101
+ console.log("[PII Guard] Deep unmasking complete across all candidates and custom fields.");
102
+ }
103
+ // ---------------------------------------------------------
104
+ // 6. Return the modified response with unmasked content
105
+ // ---------------------------------------------------------
106
+ return res;
107
+ };
108
+ }
109
+ // Helper to create a blocked response
110
+ function block(message, metadata) {
111
+ return {
112
+ finishReason: 'blocked',
113
+ output: {
114
+ type: "error",
115
+ status: "BLOCKED",
116
+ message
117
+ },
118
+ metadata
119
+ };
120
+ }
@@ -0,0 +1,10 @@
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
+ }>;
@@ -0,0 +1,47 @@
1
+ import { ModelSingleton } from '../util/singleton.js';
2
+ const REGEX_RULES = [
3
+ // EMAIL (keep your existing one)
4
+ { type: 'EMAIL', pattern: /\b[\w\.-]+@[\w\.-]+\.\w{2,}\b/gi },
5
+ // AU MOBILE (04xx xxx xxx or +61 4xx xxx xxx)
6
+ { type: 'AU_MOBILE', pattern: /\b(?:\+?61|0)4\d{2}[-\s]?\d{3}[-\s]?\d{3}\b/g },
7
+ // AU LANDLINE (02, 03, 07, 08)
8
+ { type: 'AU_LANDLINE', pattern: /\b(?:\+?61[-\s]?)?(?:2|3|7|8)\d{1}[-\s]?\d{4}[-\s]?\d{4}\b/g },
9
+ // MEDICARE NUMBER (10 digits, often grouped 4-5-1)
10
+ { type: 'MEDICARE', pattern: /\b\d{4}[-\s]?\d{5}[-\s]?\d\b/g },
11
+ // TFN (9 digits)
12
+ { type: 'TFN', pattern: /\b\d{3}[-\s]?\d{3}[-\s]?\d{3}\b/g },
13
+ // ABN (11 digits)
14
+ { type: 'ABN', pattern: /\b\d{2}[-\s]?\d{3}[-\s]?\d{3}[-\s]?\d{3}\b/g },
15
+ // CREDIT CARD (keep your existing one if needed)
16
+ { type: 'CREDIT_CARD', pattern: /\b(?:\d[ -]*?){13,16}\b/g }
17
+ ];
18
+ export async function detectPII(text, opts) {
19
+ const mode = opts?.mode ?? 'ner';
20
+ const model = opts?.model;
21
+ const results = [];
22
+ // ---- REGEX (always run) ----
23
+ for (const rule of REGEX_RULES) {
24
+ const matches = text.match(rule.pattern) || [];
25
+ matches.forEach(m => results.push({ type: rule.type, value: m }));
26
+ }
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
+ };
47
+ }
@@ -0,0 +1,19 @@
1
+ export type PiiResult = {
2
+ maskedText: string;
3
+ pii: Record<string, string>;
4
+ piiTypes: string[];
5
+ };
6
+ export declare class PiiTokenizer {
7
+ private vault;
8
+ private counter;
9
+ private piiTypes;
10
+ private createToken;
11
+ mask(text: string, matches: {
12
+ type: string;
13
+ value: string;
14
+ }[]): PiiResult;
15
+ unmask(text: string): string;
16
+ getVault(): {
17
+ [k: string]: string;
18
+ };
19
+ }
@@ -0,0 +1,32 @@
1
+ export class PiiTokenizer {
2
+ vault = new Map();
3
+ counter = 0;
4
+ piiTypes = new Set();
5
+ createToken(type) {
6
+ return `[[${type}_${this.counter++}]]`;
7
+ }
8
+ mask(text, matches) {
9
+ let masked = text;
10
+ for (const match of matches) {
11
+ const token = this.createToken(match.type);
12
+ this.vault.set(token, match.value);
13
+ this.piiTypes.add(match.type.toLowerCase());
14
+ masked = masked.split(match.value).join(token);
15
+ }
16
+ return {
17
+ maskedText: masked,
18
+ pii: Object.fromEntries(this.vault),
19
+ piiTypes: Array.from(this.piiTypes)
20
+ };
21
+ }
22
+ unmask(text) {
23
+ let result = text;
24
+ this.vault.forEach((value, token) => {
25
+ result = result.split(token).join(value); // Global replacement
26
+ });
27
+ return result;
28
+ }
29
+ getVault() {
30
+ return Object.fromEntries(this.vault);
31
+ }
32
+ }
@@ -0,0 +1,14 @@
1
+ export declare class ModelSingleton {
2
+ private static extractors;
3
+ private static nerClassifiers;
4
+ private static textClassifiers;
5
+ static init(): void;
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>;
14
+ }
@@ -0,0 +1,60 @@
1
+ import { pipeline, env } from '@huggingface/transformers';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import { fileURLToPath } from 'url';
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ env.allowRemoteModels = false;
7
+ env.localModelPath = path.join(__dirname, '../../models');
8
+ export class ModelSingleton {
9
+ static extractors = new Map();
10
+ static nerClassifiers = new Map();
11
+ static textClassifiers = new Map();
12
+ static init() {
13
+ // Always resolve model path relative to the client app, not the library
14
+ const projectRoot = process.cwd();
15
+ const modelPath = path.join(projectRoot, "models");
16
+ // Ensure directory exists
17
+ if (!fs.existsSync(modelPath)) {
18
+ fs.mkdirSync(modelPath, { recursive: true });
19
+ }
20
+ env.cacheDir = modelPath;
21
+ env.localModelPath = modelPath;
22
+ // Allow remote download if missing
23
+ env.allowRemoteModels = true;
24
+ console.log("[Guard] Using model directory:", modelPath);
25
+ }
26
+ static async getExtractor(modelName = 'Xenova/all-MiniLM-L6-v2') {
27
+ if (!this.extractors.has(modelName)) {
28
+ this.init();
29
+ const inst = await pipeline('feature-extraction', modelName);
30
+ this.extractors.set(modelName, inst);
31
+ }
32
+ return this.extractors.get(modelName);
33
+ }
34
+ static async getNER(modelName = 'Xenova/bert-base-NER') {
35
+ if (!this.nerClassifiers.has(modelName)) {
36
+ this.init();
37
+ const inst = await pipeline('token-classification', modelName);
38
+ this.nerClassifiers.set(modelName, inst);
39
+ }
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);
59
+ }
60
+ }
@@ -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,8 +4,25 @@
4
4
  "maintainers": [
5
5
  "Hemant Kohli"
6
6
  ],
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/IntFlows/genkit-guard"
10
+ },
11
+ "keywords": [
12
+ "genkit",
13
+ "genkit-guard",
14
+ "intflows",
15
+ "guardrails",
16
+ "intent-analysis",
17
+ "AI-safety",
18
+ "piimasking",
19
+ "content-moderation",
20
+ "NLP",
21
+ "transformers",
22
+ "huggingface"
23
+ ],
7
24
  "license": "Apache-2.0",
8
- "version": "0.0.7",
25
+ "version": "0.0.8-alpha.1",
9
26
  "type": "module",
10
27
  "exports": "./dist/index.js",
11
28
  "types": "./dist/index.d.ts",
@@ -20,15 +37,12 @@
20
37
  "build": "tsc",
21
38
  "prepublishOnly": "npm run build"
22
39
  },
23
- "peerDependencies": {
24
- "genkit": ">=0.5.0"
25
- },
26
40
  "dependencies": {
27
- "@huggingface/transformers": "^3.0.0",
28
- "zod": "^3.23.0"
41
+ "@huggingface/transformers": "^4.2.0",
42
+ "zod": "^4.4.3"
29
43
  },
30
44
  "devDependencies": {
31
- "typescript": "^5.6.0",
32
- "@types/node": "^20.0.0"
45
+ "@types/node": "^25.8.0",
46
+ "typescript": "^6.0.3"
33
47
  }
34
48
  }
@@ -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
  }