adaptive-memory-multi-model-router 1.9.5 → 2.0.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/.github/ISSUE_TEMPLATE/bug_report.md +50 -0
- package/.github/ISSUE_TEMPLATE/config.yml +11 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +37 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +44 -0
- package/.github/workflows/npm-stats-validation.yml +152 -0
- package/.github/workflows/pages.yml +37 -0
- package/CHANGELOG.md +122 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +110 -0
- package/LAUNCH-PAIN-DRIVEN.md +339 -0
- package/LAUNCH.md +575 -0
- package/POPULARITY_BOOSTERS.md +285 -0
- package/README.md +231 -300
- package/SECURITY.md +69 -0
- package/articles/CONTENT_STRUCTURE.md +292 -0
- package/articles/DEVTO_COST_GUIDE.md +473 -0
- package/articles/DEVTO_FINAL.md +416 -0
- package/articles/DEVTO_MULTI_PROVIDER.md +542 -0
- package/articles/DEVTO_V2_ANNOUNCEMENT.md +160 -0
- package/articles/HN_10X_BETTER.md +430 -0
- package/articles/HN_CHINESE_STYLE.md +308 -0
- package/articles/HN_FINAL.md +199 -0
- package/articles/HN_POSTED_VERSION.md +56 -0
- package/articles/HN_RESEARCH.md +364 -0
- package/articles/PAIN-DRIVEN-devto-v2.md +308 -0
- package/articles/PAIN-DRIVEN-devto-v3.md +268 -0
- package/articles/PAIN-DRIVEN-devto.md +242 -0
- package/articles/PAIN-DRIVEN-hackernews-v2.md +138 -0
- package/articles/PAIN-DRIVEN-hackernews-v3.md +151 -0
- package/articles/PAIN-DRIVEN-hackernews.md +131 -0
- package/articles/PAIN-DRIVEN-reddit-v2.md +301 -0
- package/articles/PAIN-DRIVEN-reddit-v3.md +236 -0
- package/articles/PAIN-DRIVEN-reddit.md +218 -0
- package/articles/PAIN-DRIVEN-twitter-v2.md +110 -0
- package/articles/PAIN-DRIVEN-twitter-v3.md +121 -0
- package/articles/PAIN-DRIVEN-twitter.md +120 -0
- package/articles/PORTKEY_VS_A3M.md +147 -0
- package/articles/REDDIT_FINAL.md +232 -0
- package/articles/TWITTER_FINAL.md +167 -0
- package/articles/WHY_10X_BETTER.md +261 -0
- package/articles/WHY_CHINESE_STYLE_BETTER.md +323 -0
- package/articles/hashnode-llm-cost-optimization.md +125 -0
- package/articles/medium-building-llm-router.md +205 -0
- package/articles/twitter-thread-cost-savings.md +98 -0
- package/articles/youtube-tutorial-script.md +262 -0
- package/assets/banner.svg +109 -0
- package/assets/logo.svg +68 -0
- package/assets/social-preview.svg +64 -0
- package/demo/demo-script.md +53 -0
- package/dist/analytics/costAnalytics.d.ts +77 -0
- package/dist/analytics/costAnalytics.d.ts.map +1 -0
- package/dist/analytics/costAnalytics.js +219 -0
- package/dist/analytics/costAnalytics.js.map +1 -0
- package/dist/cache/semanticCache.d.ts +62 -0
- package/dist/cache/semanticCache.d.ts.map +1 -0
- package/dist/cache/semanticCache.js +176 -0
- package/dist/cache/semanticCache.js.map +1 -0
- package/dist/cli.js +35 -0
- package/dist/index.d.ts +30 -722
- package/dist/index.js +76 -355
- package/dist/index.js.map +1 -1
- package/dist/integrations/langchainAdapter.d.ts +146 -0
- package/dist/integrations/langchainAdapter.d.ts.map +1 -0
- package/dist/integrations/langchainAdapter.js +731 -0
- package/dist/integrations/langchainAdapter.js.map +1 -0
- package/dist/integrations/oauth.d.ts +69 -0
- package/dist/integrations/oauth.d.ts.map +1 -0
- package/dist/integrations/oauth.js +225 -21
- package/dist/integrations/oauth.js.map +1 -0
- package/dist/memory/autoFetch.d.ts +39 -0
- package/dist/memory/autoFetch.d.ts.map +1 -0
- package/dist/memory/autoFetch.js +80 -88
- package/dist/memory/autoFetch.js.map +1 -0
- package/dist/memory/memoryTree.d.ts +76 -0
- package/dist/memory/memoryTree.d.ts.map +1 -0
- package/dist/memory/memoryTree.js +185 -130
- package/dist/memory/memoryTree.js.map +1 -0
- package/dist/memory/obsidianVault.d.ts +71 -0
- package/dist/memory/obsidianVault.d.ts.map +1 -0
- package/dist/memory/obsidianVault.js +207 -22
- package/dist/memory/obsidianVault.js.map +1 -0
- package/dist/providers/providerConfig.d.ts +49 -0
- package/dist/providers/providerConfig.d.ts.map +1 -0
- package/dist/providers/providerConfig.js +806 -401
- package/dist/providers/providerConfig.js.map +1 -0
- package/dist/security/guardrails.d.ts +76 -0
- package/dist/security/guardrails.d.ts.map +1 -0
- package/dist/security/guardrails.js +479 -0
- package/dist/security/guardrails.js.map +1 -0
- package/dist/server/dashboard.d.ts +58 -0
- package/dist/server/dashboard.d.ts.map +1 -0
- package/dist/server/dashboard.js +553 -0
- package/dist/server/dashboard.js.map +1 -0
- package/dist/server/modelMapper.d.ts +43 -0
- package/dist/server/modelMapper.d.ts.map +1 -0
- package/dist/server/modelMapper.js +154 -0
- package/dist/server/modelMapper.js.map +1 -0
- package/dist/server/proxyServer.d.ts +41 -0
- package/dist/server/proxyServer.d.ts.map +1 -0
- package/dist/server/proxyServer.js +932 -0
- package/dist/server/proxyServer.js.map +1 -0
- package/dist/skills/__tests__/skill_manager.test.d.ts +2 -0
- package/dist/skills/__tests__/skill_manager.test.d.ts.map +1 -0
- package/dist/skills/__tests__/skill_manager.test.js +268 -0
- package/dist/skills/__tests__/skill_manager.test.js.map +1 -0
- package/docs-site/index.html +347 -0
- package/package.json +53 -7
- package/playground/README.md +51 -0
- package/playground/codesandbox.json +12 -0
- package/playground/index.js +39 -0
- package/scripts/update-npm-badges.js +158 -0
- package/src/analytics/costAnalytics.ts +304 -0
- package/src/cache/semanticCache.ts +221 -0
- package/src/index.ts +99 -0
- package/src/integrations/langchainAdapter.ts +955 -0
- package/src/providers/providerConfig.ts +923 -0
- package/src/security/guardrails.ts +585 -0
- package/src/server/dashboard.ts +610 -0
- package/src/server/modelMapper.ts +182 -0
- package/src/server/proxyServer.ts +1105 -0
- package/src/types/langchain.d.ts +83 -0
- package/tsconfig.build.json +20 -0
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A3M Router - Guardrails Engine
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive input/output guardrail system for production LLM routing:
|
|
5
|
+
* - Prompt injection detection (score-based 0-100)
|
|
6
|
+
* - PII detection and redaction
|
|
7
|
+
* - Content filtering with configurable blocklist
|
|
8
|
+
* - Language detection for intelligent routing
|
|
9
|
+
* - Output validation and quality checks
|
|
10
|
+
* - Custom user-defined guardrails
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Types
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
export interface GuardrailResult {
|
|
18
|
+
passed: boolean;
|
|
19
|
+
blocked: boolean;
|
|
20
|
+
reason?: string;
|
|
21
|
+
modified?: string;
|
|
22
|
+
metadata?: Record<string, any>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface GuardrailConfig {
|
|
26
|
+
// Input guardrails
|
|
27
|
+
promptInjection: boolean;
|
|
28
|
+
piiDetection: boolean;
|
|
29
|
+
contentFilter: boolean;
|
|
30
|
+
maxLength: number;
|
|
31
|
+
languageDetection: boolean;
|
|
32
|
+
|
|
33
|
+
// Output guardrails
|
|
34
|
+
outputFilter: boolean;
|
|
35
|
+
outputPII: boolean;
|
|
36
|
+
hallucinationCheck: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type GuardrailCheck = (content: string) => GuardrailResult;
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Default configuration
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
const DEFAULT_CONFIG: GuardrailConfig = {
|
|
46
|
+
promptInjection: true,
|
|
47
|
+
piiDetection: true,
|
|
48
|
+
contentFilter: true,
|
|
49
|
+
maxLength: 100_000,
|
|
50
|
+
languageDetection: false,
|
|
51
|
+
outputFilter: true,
|
|
52
|
+
outputPII: true,
|
|
53
|
+
hallucinationCheck: true,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Prompt-injection detection
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
const INJECTION_PATTERNS: Array<{ pattern: RegExp; weight: number; label: string }> = [
|
|
61
|
+
{ pattern: /ignore\s+(?:previous|above|earlier|all)\s+(?:instructions?|prompts?|rules?)/i, weight: 30, label: 'ignore-instructions' },
|
|
62
|
+
{ pattern: /disregard\s+(?:previous|above|earlier|all)/i, weight: 25, label: 'disregard' },
|
|
63
|
+
{ pattern: /forget\s+(?:previous|above|earlier|everything|all)/i, weight: 20, label: 'forget' },
|
|
64
|
+
{ pattern: /^system\s*:/im, weight: 35, label: 'system-prefix' },
|
|
65
|
+
{ pattern: /\[\s*system\s*\]/i, weight: 30, label: 'system-bracket' },
|
|
66
|
+
{ pattern: /<\s*system\s*>/i, weight: 30, label: 'system-tag' },
|
|
67
|
+
{ pattern: /\{\s*system\s*\}/i, weight: 25, label: 'system-brace' },
|
|
68
|
+
{ pattern: /you\s+are\s+now\s+/i, weight: 25, label: 'you-are-now' },
|
|
69
|
+
{ pattern: /new\s+instruction/i, weight: 20, label: 'new-instruction' },
|
|
70
|
+
{ pattern: /override\s+(?:previous|settings|safety|filter)/i, weight: 30, label: 'override' },
|
|
71
|
+
{ pattern: /bypass\s+(?:filter|restriction|safety|guard|security)/i, weight: 30, label: 'bypass' },
|
|
72
|
+
{ pattern: /DAN\s*[\(\[]/i, weight: 35, label: 'dan-attack' },
|
|
73
|
+
{ pattern: /jailbreak/i, weight: 35, label: 'jailbreak' },
|
|
74
|
+
{ pattern: /act\s+as\s+(?:if\s+you|a\s+different)/i, weight: 15, label: 'act-as' },
|
|
75
|
+
{ pattern: /pretend\s+(?:you\s+are|to\s+be)/i, weight: 15, label: 'pretend' },
|
|
76
|
+
{ pattern: /simulate\s+(?:a\s+)?(?:different|new)\s+(?:persona|identity|character)/i, weight: 15, label: 'simulate-persona' },
|
|
77
|
+
{ pattern: /reveal\s+(?:your|the|hidden|secret)/i, weight: 10, label: 'reveal-secrets' },
|
|
78
|
+
{ pattern: /output\s+(?:your|the)\s+(?:system|initial|original)\s+(?:prompt|instructions?)/i, weight: 25, label: 'extract-prompt' },
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
function scoreInjection(text: string): { score: number; triggers: string[] } {
|
|
82
|
+
let score = 0;
|
|
83
|
+
const triggers: string[] = [];
|
|
84
|
+
|
|
85
|
+
for (const { pattern, weight, label } of INJECTION_PATTERNS) {
|
|
86
|
+
if (pattern.test(text)) {
|
|
87
|
+
score += weight;
|
|
88
|
+
triggers.push(label);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Clamp to 0-100
|
|
93
|
+
return { score: Math.min(score, 100), triggers };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// PII detection & redaction
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
const PII_REDACTION_RULES: Array<{
|
|
101
|
+
type: string;
|
|
102
|
+
pattern: RegExp;
|
|
103
|
+
replacement: string;
|
|
104
|
+
}> = [
|
|
105
|
+
{
|
|
106
|
+
type: 'email',
|
|
107
|
+
pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
|
|
108
|
+
replacement: '[EMAIL_REDACTED]',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
type: 'phone',
|
|
112
|
+
pattern: /(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
|
|
113
|
+
replacement: '[PHONE_REDACTED]',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
type: 'ssn',
|
|
117
|
+
pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
|
|
118
|
+
replacement: '[SSN_REDACTED]',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
type: 'creditCard',
|
|
122
|
+
pattern: /\b(?:\d[ -]*?){13,19}\b/g,
|
|
123
|
+
replacement: '[CC_REDACTED]',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
type: 'apiKey',
|
|
127
|
+
pattern: /\b(?:sk-[a-zA-Z0-9]{20,}|key-[a-zA-Z0-9]{20,}|AKIA[0-9A-Z]{16})\b/g,
|
|
128
|
+
replacement: '[API_KEY_REDACTED]',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
type: 'ipAddress',
|
|
132
|
+
pattern: /\b(?:(?:25[0-5]|2[0-4]\d|1?\d{1,2})\.){3}(?:25[0-5]|2[0-4]\d|1?\d{1,2})\b/g,
|
|
133
|
+
replacement: '[IP_REDACTED]',
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
function detectPII(text: string): { types: string[]; matches: Record<string, number> } {
|
|
138
|
+
const types: string[] = [];
|
|
139
|
+
const matches: Record<string, number> = {};
|
|
140
|
+
|
|
141
|
+
for (const rule of PII_REDACTION_RULES) {
|
|
142
|
+
// Clone regex because .test() advances lastIndex on global regexes
|
|
143
|
+
const re = new RegExp(rule.pattern.source, rule.pattern.flags);
|
|
144
|
+
const found = text.match(re);
|
|
145
|
+
if (found && found.length > 0) {
|
|
146
|
+
types.push(rule.type);
|
|
147
|
+
matches[rule.type] = found.length;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { types, matches };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function redactPII(text: string): string {
|
|
155
|
+
let result = text;
|
|
156
|
+
for (const rule of PII_REDACTION_RULES) {
|
|
157
|
+
result = result.replace(rule.pattern, rule.replacement);
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Content filter
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
interface ContentRule {
|
|
167
|
+
category: string;
|
|
168
|
+
pattern: RegExp;
|
|
169
|
+
severity: 'warn' | 'block';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const DEFAULT_CONTENT_RULES: ContentRule[] = [
|
|
173
|
+
// Violence / hate
|
|
174
|
+
{ category: 'hate', pattern: /\b(?:kill\s+(?:all|every|those)|ethnic\s+cleansing|genocide)\b/i, severity: 'block' },
|
|
175
|
+
{ category: 'violence', pattern: /\b(?:how\s+to\s+(?:make|build|create)\s+(?:bomb|weapon|explosive))\b/i, severity: 'block' },
|
|
176
|
+
// Self-harm
|
|
177
|
+
{ category: 'selfHarm', pattern: /\b(?:how\s+to\s+(?:commit|do)\s+(?:suicide|self\s*harm)|kill\s+myself)\b/i, severity: 'block' },
|
|
178
|
+
// Exploitation
|
|
179
|
+
{ category: 'exploitation', pattern: /\b(?:child\s+(?:abuse|exploitation|pornography))\b/i, severity: 'block' },
|
|
180
|
+
// Illegal activity
|
|
181
|
+
{ category: 'illegal', pattern: /\b(?:how\s+to\s+(?:hack|steal|commit\s+fraud))\b/i, severity: 'warn' },
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
function filterContent(text: string, rules: ContentRule[]): { flagged: boolean; categories: string[]; action: 'pass' | 'warn' | 'block' } {
|
|
185
|
+
const categories: string[] = [];
|
|
186
|
+
let maxSeverity: 'pass' | 'warn' | 'block' = 'pass';
|
|
187
|
+
|
|
188
|
+
for (const rule of rules) {
|
|
189
|
+
if (rule.pattern.test(text)) {
|
|
190
|
+
categories.push(rule.category);
|
|
191
|
+
if (rule.severity === 'block') {
|
|
192
|
+
maxSeverity = 'block';
|
|
193
|
+
} else if (maxSeverity === 'pass') {
|
|
194
|
+
maxSeverity = 'warn';
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { flagged: categories.length > 0, categories, action: maxSeverity };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Language detection (script-based)
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
export type DetectedLanguage = 'latin' | 'cjk' | 'cyrillic' | 'arabic' | 'devanagari' | 'mixed' | 'unknown';
|
|
207
|
+
|
|
208
|
+
interface LanguageDetectionResult {
|
|
209
|
+
primary: DetectedLanguage;
|
|
210
|
+
details: Record<DetectedLanguage, number>;
|
|
211
|
+
suggestedProviders: string[];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const LANGUAGE_PROVIDER_HINTS: Record<DetectedLanguage, string[]> = {
|
|
215
|
+
latin: ['openai', 'anthropic', 'google', 'groq', 'mistral'],
|
|
216
|
+
cjk: ['google', 'openai', 'anthropic'],
|
|
217
|
+
cyrillic: ['openai', 'anthropic', 'google', 'mistral'],
|
|
218
|
+
arabic: ['google', 'openai', 'anthropic'],
|
|
219
|
+
devanagari: ['google', 'openai', 'anthropic'],
|
|
220
|
+
mixed: ['google', 'openai', 'anthropic'],
|
|
221
|
+
unknown: ['openai', 'anthropic', 'google'],
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
function detectLanguage(text: string): LanguageDetectionResult {
|
|
225
|
+
const scripts: Record<DetectedLanguage, number> = {
|
|
226
|
+
latin: 0,
|
|
227
|
+
cjk: 0,
|
|
228
|
+
cyrillic: 0,
|
|
229
|
+
arabic: 0,
|
|
230
|
+
devanagari: 0,
|
|
231
|
+
mixed: 0,
|
|
232
|
+
unknown: 0,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
for (const ch of text) {
|
|
236
|
+
const cp = ch.codePointAt(0)!;
|
|
237
|
+
if (cp >= 0x0041 && cp <= 0x024F) { scripts.latin++; }
|
|
238
|
+
else if ((cp >= 0x4E00 && cp <= 0x9FFF) || (cp >= 0x3040 && cp <= 0x309F) || (cp >= 0x30A0 && cp <= 0x30FF) || (cp >= 0xAC00 && cp <= 0xD7AF)) { scripts.cjk++; }
|
|
239
|
+
else if (cp >= 0x0400 && cp <= 0x04FF) { scripts.cyrillic++; }
|
|
240
|
+
else if (cp >= 0x0600 && cp <= 0x06FF) { scripts.arabic++; }
|
|
241
|
+
else if (cp >= 0x0900 && cp <= 0x097F) { scripts.devanagari++; }
|
|
242
|
+
else if (cp > 0x7F && !/\s/.test(ch)) { scripts.unknown++; }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Determine primary
|
|
246
|
+
const relevant = (['latin', 'cjk', 'cyrillic', 'arabic', 'devanagari'] as DetectedLanguage[])
|
|
247
|
+
.filter((s) => scripts[s] > 0);
|
|
248
|
+
|
|
249
|
+
let primary: DetectedLanguage = 'unknown';
|
|
250
|
+
if (relevant.length === 0) {
|
|
251
|
+
primary = 'latin'; // Default for pure ASCII
|
|
252
|
+
} else if (relevant.length === 1) {
|
|
253
|
+
primary = relevant[0];
|
|
254
|
+
} else {
|
|
255
|
+
primary = 'mixed';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
primary,
|
|
260
|
+
details: scripts,
|
|
261
|
+
suggestedProviders: LANGUAGE_PROVIDER_HINTS[primary],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// Output validation
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
interface OutputValidationResult {
|
|
270
|
+
valid: boolean;
|
|
271
|
+
issues: string[];
|
|
272
|
+
qualityScore: number; // 0-100
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function validateOutput(input: string, output: string): OutputValidationResult {
|
|
276
|
+
const issues: string[] = [];
|
|
277
|
+
let qualityScore = 100;
|
|
278
|
+
|
|
279
|
+
// Empty output
|
|
280
|
+
if (!output || output.trim().length === 0) {
|
|
281
|
+
issues.push('empty_output');
|
|
282
|
+
qualityScore -= 50;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Very short output for non-trivial input
|
|
286
|
+
if (input.length > 100 && output.trim().length < 10) {
|
|
287
|
+
issues.push('suspiciously_short');
|
|
288
|
+
qualityScore -= 20;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Repetition detection
|
|
292
|
+
const words = output.split(/\s+/);
|
|
293
|
+
if (words.length > 20) {
|
|
294
|
+
const unique = new Set(words.map((w) => w.toLowerCase()));
|
|
295
|
+
const ratio = unique.size / words.length;
|
|
296
|
+
if (ratio < 0.3) {
|
|
297
|
+
issues.push('high_repetition');
|
|
298
|
+
qualityScore -= 25;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// GPT refusal patterns
|
|
303
|
+
const refusalPatterns = [
|
|
304
|
+
/I\s+(?:can'?t|cannot|won't|am\s+not\s+able\s+to)\s+(?:help|assist|do\s+that|provide)/i,
|
|
305
|
+
/As\s+an?\s+AI/i,
|
|
306
|
+
/I'?m\s+(?:sorry|unable)/i,
|
|
307
|
+
];
|
|
308
|
+
for (const p of refusalPatterns) {
|
|
309
|
+
if (p.test(output)) {
|
|
310
|
+
issues.push('refusal_detected');
|
|
311
|
+
qualityScore -= 10;
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Hallucination heuristic: output repeats the question verbatim without answering
|
|
317
|
+
const inputLower = input.toLowerCase().trim();
|
|
318
|
+
const outputLower = output.toLowerCase().trim();
|
|
319
|
+
if (outputLower === inputLower) {
|
|
320
|
+
issues.push('echo_response');
|
|
321
|
+
qualityScore -= 30;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
valid: qualityScore >= 20,
|
|
326
|
+
issues,
|
|
327
|
+
qualityScore: Math.max(0, qualityScore),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// GuardrailEngine
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
|
|
335
|
+
export class GuardrailEngine {
|
|
336
|
+
private config: GuardrailConfig;
|
|
337
|
+
private customGuardrails: Map<string, GuardrailCheck> = new Map();
|
|
338
|
+
private contentRules: ContentRule[];
|
|
339
|
+
private blocklist: string[] = [];
|
|
340
|
+
|
|
341
|
+
constructor(config?: Partial<GuardrailConfig>) {
|
|
342
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
343
|
+
this.contentRules = [...DEFAULT_CONTENT_RULES];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ---- Public API ----
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Check input content before routing to an LLM.
|
|
350
|
+
*/
|
|
351
|
+
async checkInput(content: string): Promise<GuardrailResult> {
|
|
352
|
+
const metadata: Record<string, any> = {};
|
|
353
|
+
|
|
354
|
+
// Length check
|
|
355
|
+
if (content.length > this.config.maxLength) {
|
|
356
|
+
return {
|
|
357
|
+
passed: false,
|
|
358
|
+
blocked: true,
|
|
359
|
+
reason: `Input exceeds maximum length (${content.length} > ${this.config.maxLength})`,
|
|
360
|
+
modified: content.substring(0, this.config.maxLength),
|
|
361
|
+
metadata: { truncated: true, originalLength: content.length },
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let modified = content;
|
|
366
|
+
|
|
367
|
+
// Prompt injection
|
|
368
|
+
if (this.config.promptInjection) {
|
|
369
|
+
const injection = scoreInjection(content);
|
|
370
|
+
metadata.injectionScore = injection.score;
|
|
371
|
+
metadata.injectionTriggers = injection.triggers;
|
|
372
|
+
|
|
373
|
+
if (injection.score >= 80) {
|
|
374
|
+
return {
|
|
375
|
+
passed: false,
|
|
376
|
+
blocked: true,
|
|
377
|
+
reason: `Prompt injection detected (score: ${injection.score})`,
|
|
378
|
+
metadata,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
if (injection.score >= 50) {
|
|
382
|
+
metadata.injectionWarning = true;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// PII detection + redaction
|
|
387
|
+
if (this.config.piiDetection) {
|
|
388
|
+
const pii = detectPII(content);
|
|
389
|
+
metadata.piiTypes = pii.types;
|
|
390
|
+
metadata.piiCounts = pii.matches;
|
|
391
|
+
|
|
392
|
+
if (pii.types.length > 0) {
|
|
393
|
+
modified = redactPII(modified);
|
|
394
|
+
metadata.piiRedacted = true;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Content filter
|
|
399
|
+
if (this.config.contentFilter) {
|
|
400
|
+
const contentResult = filterContent(modified, this.contentRules);
|
|
401
|
+
metadata.contentFlagged = contentResult.flagged;
|
|
402
|
+
metadata.contentCategories = contentResult.categories;
|
|
403
|
+
|
|
404
|
+
if (contentResult.action === 'block') {
|
|
405
|
+
return {
|
|
406
|
+
passed: false,
|
|
407
|
+
blocked: true,
|
|
408
|
+
reason: `Content blocked: ${contentResult.categories.join(', ')}`,
|
|
409
|
+
metadata,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
if (contentResult.action === 'warn') {
|
|
413
|
+
metadata.contentWarning = true;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Blocklist check
|
|
418
|
+
if (this.blocklist.length > 0) {
|
|
419
|
+
const lowerContent = modified.toLowerCase();
|
|
420
|
+
for (const term of this.blocklist) {
|
|
421
|
+
if (lowerContent.includes(term.toLowerCase())) {
|
|
422
|
+
return {
|
|
423
|
+
passed: false,
|
|
424
|
+
blocked: true,
|
|
425
|
+
reason: `Content blocked by blocklist: "${term}"`,
|
|
426
|
+
metadata,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Language detection
|
|
433
|
+
if (this.config.languageDetection) {
|
|
434
|
+
const lang = detectLanguage(modified);
|
|
435
|
+
metadata.language = lang.primary;
|
|
436
|
+
metadata.suggestedProviders = lang.suggestedProviders;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Custom guardrails
|
|
440
|
+
for (const [name, check] of this.customGuardrails) {
|
|
441
|
+
const result = check(modified);
|
|
442
|
+
if (result.blocked) {
|
|
443
|
+
return {
|
|
444
|
+
passed: false,
|
|
445
|
+
blocked: true,
|
|
446
|
+
reason: result.reason || `Blocked by custom guardrail: ${name}`,
|
|
447
|
+
metadata: { ...metadata, ...result.metadata, blockedBy: name },
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
if (result.modified) {
|
|
451
|
+
modified = result.modified;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
passed: true,
|
|
457
|
+
blocked: false,
|
|
458
|
+
modified: modified !== content ? modified : undefined,
|
|
459
|
+
metadata,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Check output content before returning to caller.
|
|
465
|
+
*/
|
|
466
|
+
async checkOutput(input: string, output: string): Promise<GuardrailResult> {
|
|
467
|
+
const metadata: Record<string, any> = {};
|
|
468
|
+
let modified = output;
|
|
469
|
+
|
|
470
|
+
// PII in output
|
|
471
|
+
if (this.config.outputPII) {
|
|
472
|
+
const pii = detectPII(output);
|
|
473
|
+
metadata.piiTypes = pii.types;
|
|
474
|
+
if (pii.types.length > 0) {
|
|
475
|
+
modified = redactPII(modified);
|
|
476
|
+
metadata.piiRedacted = true;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Content filter on output
|
|
481
|
+
if (this.config.outputFilter) {
|
|
482
|
+
const contentResult = filterContent(modified, this.contentRules);
|
|
483
|
+
metadata.contentFlagged = contentResult.flagged;
|
|
484
|
+
metadata.contentCategories = contentResult.categories;
|
|
485
|
+
|
|
486
|
+
if (contentResult.action === 'block') {
|
|
487
|
+
return {
|
|
488
|
+
passed: false,
|
|
489
|
+
blocked: true,
|
|
490
|
+
reason: `Output blocked: ${contentResult.categories.join(', ')}`,
|
|
491
|
+
metadata,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Quality / hallucination checks
|
|
497
|
+
if (this.config.hallucinationCheck) {
|
|
498
|
+
const validation = validateOutput(input, modified);
|
|
499
|
+
metadata.qualityScore = validation.qualityScore;
|
|
500
|
+
metadata.issues = validation.issues;
|
|
501
|
+
|
|
502
|
+
if (!validation.valid) {
|
|
503
|
+
return {
|
|
504
|
+
passed: false,
|
|
505
|
+
blocked: true,
|
|
506
|
+
reason: `Output quality check failed: ${validation.issues.join(', ')}`,
|
|
507
|
+
metadata,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Custom guardrails on output
|
|
513
|
+
for (const [name, check] of this.customGuardrails) {
|
|
514
|
+
const result = check(modified);
|
|
515
|
+
if (result.blocked) {
|
|
516
|
+
return {
|
|
517
|
+
passed: false,
|
|
518
|
+
blocked: true,
|
|
519
|
+
reason: result.reason || `Output blocked by custom guardrail: ${name}`,
|
|
520
|
+
metadata: { ...metadata, ...result.metadata, blockedBy: name },
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
if (result.modified) {
|
|
524
|
+
modified = result.modified;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return {
|
|
529
|
+
passed: true,
|
|
530
|
+
blocked: false,
|
|
531
|
+
modified: modified !== output ? modified : undefined,
|
|
532
|
+
metadata,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Register a custom guardrail check function.
|
|
538
|
+
*/
|
|
539
|
+
addGuardrail(name: string, check: GuardrailCheck): void {
|
|
540
|
+
this.customGuardrails.set(name, check);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Remove a custom guardrail.
|
|
545
|
+
*/
|
|
546
|
+
removeGuardrail(name: string): boolean {
|
|
547
|
+
return this.customGuardrails.delete(name);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Add a term to the blocklist.
|
|
552
|
+
*/
|
|
553
|
+
addBlocklistTerm(term: string): void {
|
|
554
|
+
this.blocklist.push(term);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Add a content filter rule.
|
|
559
|
+
*/
|
|
560
|
+
addContentRule(rule: ContentRule): void {
|
|
561
|
+
this.contentRules.push(rule);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Get the current configuration.
|
|
566
|
+
*/
|
|
567
|
+
getConfig(): Readonly<GuardrailConfig> {
|
|
568
|
+
return { ...this.config };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Update configuration.
|
|
573
|
+
*/
|
|
574
|
+
updateConfig(partial: Partial<GuardrailConfig>): void {
|
|
575
|
+
Object.assign(this.config, partial);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ---------------------------------------------------------------------------
|
|
580
|
+
// Convenience factory
|
|
581
|
+
// ---------------------------------------------------------------------------
|
|
582
|
+
|
|
583
|
+
export function createGuardrails(config?: Partial<GuardrailConfig>): GuardrailEngine {
|
|
584
|
+
return new GuardrailEngine(config);
|
|
585
|
+
}
|