plugin-sensitive-filter-xr 0.0.2 → 0.0.5
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 +156 -54
- package/dist/lib/sensitive-filter.module.d.ts.map +1 -1
- package/dist/lib/sensitive-filter.module.js +2 -1
- package/dist/lib/sensitiveFilter.d.ts +6 -3
- package/dist/lib/sensitiveFilter.d.ts.map +1 -1
- package/dist/lib/sensitiveFilter.js +782 -83
- package/dist/lib/types.d.ts +143 -29
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js +37 -21
- package/package.json +1 -1
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import { __decorate } from "tslib";
|
|
1
|
+
import { __decorate, __metadata } from "tslib";
|
|
2
2
|
import { z as z4 } from 'zod/v4';
|
|
3
3
|
import { AIMessage, HumanMessage } from '@langchain/core/messages';
|
|
4
|
-
import { Injectable } from '@nestjs/common';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
5
|
+
import { CommandBus } from '@nestjs/cqrs';
|
|
6
|
+
import { AgentMiddlewareStrategy, CreateModelClientCommand, WrapWorkflowNodeExecutionCommand, } from '@xpert-ai/plugin-sdk';
|
|
7
|
+
import { SensitiveFilterIcon, llmDecisionSchema, resolveGeneralPackRules, sensitiveFilterConfigSchema, } from './types.js';
|
|
7
8
|
const SENSITIVE_FILTER_MIDDLEWARE_NAME = 'SensitiveFilterMiddleware';
|
|
8
9
|
const DEFAULT_INPUT_BLOCK_MESSAGE = '输入内容触发敏感策略,已拦截。';
|
|
9
10
|
const DEFAULT_OUTPUT_BLOCK_MESSAGE = '输出内容触发敏感策略,已拦截。';
|
|
10
11
|
const DEFAULT_REWRITE_TEXT = '[已过滤]';
|
|
12
|
+
const CONFIG_PARSE_ERROR = '敏感词过滤配置格式不正确,请检查填写内容。';
|
|
13
|
+
const BUSINESS_RULES_VALIDATION_ERROR = '请至少配置 1 条有效业务规则(pattern/type/action/scope/severity),或启用通用规则包。';
|
|
14
|
+
const LLM_MODE_VALIDATION_ERROR = '请完善 LLM 过滤配置:需填写过滤模型、生效范围、审核规则说明。';
|
|
11
15
|
function isRecord(value) {
|
|
12
16
|
return typeof value === 'object' && value !== null;
|
|
13
17
|
}
|
|
@@ -62,9 +66,6 @@ function extractInputText(state, runtime) {
|
|
|
62
66
|
}
|
|
63
67
|
return '';
|
|
64
68
|
}
|
|
65
|
-
function escapeRegExp(value) {
|
|
66
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
67
|
-
}
|
|
68
69
|
function getSeverityWeight(severity) {
|
|
69
70
|
return severity === 'high' ? 2 : 1;
|
|
70
71
|
}
|
|
@@ -168,7 +169,7 @@ function normalizeRuleDrafts(input) {
|
|
|
168
169
|
const rules = [];
|
|
169
170
|
for (const [index, draft] of input.entries()) {
|
|
170
171
|
if (!isRecord(draft)) {
|
|
171
|
-
|
|
172
|
+
continue;
|
|
172
173
|
}
|
|
173
174
|
const id = toNonEmptyString(draft['id']);
|
|
174
175
|
const pattern = toNonEmptyString(draft['pattern']);
|
|
@@ -177,44 +178,175 @@ function normalizeRuleDrafts(input) {
|
|
|
177
178
|
const severity = toNonEmptyString(draft['severity']);
|
|
178
179
|
const action = toNonEmptyString(draft['action']);
|
|
179
180
|
const replacementText = toNonEmptyString(draft['replacementText']) ?? undefined;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (!type) {
|
|
184
|
-
throw new Error(`Invalid rule at index ${index}: type is required`);
|
|
185
|
-
}
|
|
186
|
-
if (!action) {
|
|
187
|
-
throw new Error(`Invalid rule at index ${index}: action is required`);
|
|
181
|
+
const hasAnyValue = Boolean(id || pattern || type || scope || severity || action || replacementText);
|
|
182
|
+
if (!hasAnyValue) {
|
|
183
|
+
continue;
|
|
188
184
|
}
|
|
189
|
-
if (action
|
|
190
|
-
|
|
185
|
+
if (!pattern || !type || !action || !scope || !severity) {
|
|
186
|
+
continue;
|
|
191
187
|
}
|
|
192
188
|
if (!['keyword', 'regex'].includes(type)) {
|
|
193
|
-
|
|
189
|
+
continue;
|
|
194
190
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
throw new Error(`Invalid rule at index ${index}: unsupported scope '${normalizedScope}'`);
|
|
191
|
+
if (!['input', 'output', 'both'].includes(scope)) {
|
|
192
|
+
continue;
|
|
198
193
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
throw new Error(`Invalid rule at index ${index}: unsupported severity '${normalizedSeverity}'`);
|
|
194
|
+
if (!['high', 'medium'].includes(severity)) {
|
|
195
|
+
continue;
|
|
202
196
|
}
|
|
203
197
|
if (!['block', 'rewrite'].includes(action)) {
|
|
204
|
-
|
|
198
|
+
continue;
|
|
205
199
|
}
|
|
206
200
|
rules.push({
|
|
207
201
|
id: id ?? `rule-${index + 1}`,
|
|
208
202
|
pattern,
|
|
209
203
|
type,
|
|
210
|
-
scope
|
|
211
|
-
severity
|
|
204
|
+
scope,
|
|
205
|
+
severity,
|
|
212
206
|
action,
|
|
213
207
|
replacementText,
|
|
214
208
|
});
|
|
215
209
|
}
|
|
216
210
|
return rules;
|
|
217
211
|
}
|
|
212
|
+
function modeIncludesScope(scope, phase) {
|
|
213
|
+
return scope === 'both' || scope === phase;
|
|
214
|
+
}
|
|
215
|
+
function buildInternalLlmPrompt(rulePrompt) {
|
|
216
|
+
return ('你是敏感内容审核器。请根据用户给出的审核规则判断文本是否命中敏感内容。' +
|
|
217
|
+
'\n你只能返回 JSON,不要输出额外说明。' +
|
|
218
|
+
'\nJSON字段: matched(boolean), action("block"|"rewrite"), replacementText(string), reason(string), categories(string[]).' +
|
|
219
|
+
'\n命中时请尽量给出 replacementText;未命中时返回 {"matched": false}。' +
|
|
220
|
+
'\n说明:系统会统一执行改写策略,即便你返回 action=block 也会按 rewrite 处理。' +
|
|
221
|
+
`\n\n用户审核规则:\n${rulePrompt}`);
|
|
222
|
+
}
|
|
223
|
+
function parseLlmDecision(raw, rewriteFallbackText) {
|
|
224
|
+
let payload = raw;
|
|
225
|
+
if (typeof payload === 'string') {
|
|
226
|
+
try {
|
|
227
|
+
payload = JSON.parse(payload);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
throw new Error('LLM decision is not valid JSON string');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (isRecord(payload) && !('matched' in payload) && 'content' in payload) {
|
|
234
|
+
const content = extractPrimitiveText(payload['content']).trim();
|
|
235
|
+
if (!content) {
|
|
236
|
+
throw new Error('LLM decision content is empty');
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
payload = JSON.parse(content);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
throw new Error('LLM decision content is not valid JSON');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const parsed = llmDecisionSchema.safeParse(payload);
|
|
246
|
+
if (!parsed.success) {
|
|
247
|
+
throw new Error(`Invalid LLM decision: ${z4.prettifyError(parsed.error)}`);
|
|
248
|
+
}
|
|
249
|
+
const decision = parsed.data;
|
|
250
|
+
const reason = toNonEmptyString(decision.reason) ?? undefined;
|
|
251
|
+
const categories = Array.isArray(decision.categories) ? decision.categories.filter(Boolean) : undefined;
|
|
252
|
+
if (!decision.matched) {
|
|
253
|
+
return {
|
|
254
|
+
matched: false,
|
|
255
|
+
reason,
|
|
256
|
+
categories,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
matched: true,
|
|
261
|
+
action: 'rewrite',
|
|
262
|
+
replacementText: toNonEmptyString(decision.replacementText) ?? rewriteFallbackText,
|
|
263
|
+
reason,
|
|
264
|
+
categories,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function resolveRuntimeLlmConfig(config) {
|
|
268
|
+
if (!isRecord(config)) {
|
|
269
|
+
throw new Error(LLM_MODE_VALIDATION_ERROR);
|
|
270
|
+
}
|
|
271
|
+
const model = isRecord(config.model) ? config.model : null;
|
|
272
|
+
const scope = toNonEmptyString(config.scope);
|
|
273
|
+
const rulePrompt = toNonEmptyString(config.rulePrompt) ?? toNonEmptyString(config.systemPrompt);
|
|
274
|
+
if (!model || !scope || !rulePrompt) {
|
|
275
|
+
throw new Error(LLM_MODE_VALIDATION_ERROR);
|
|
276
|
+
}
|
|
277
|
+
const outputMethodRaw = toNonEmptyString(config.outputMethod);
|
|
278
|
+
const outputMethod = ['functionCalling', 'jsonMode', 'jsonSchema'].includes(outputMethodRaw ?? '')
|
|
279
|
+
? outputMethodRaw
|
|
280
|
+
: 'jsonMode';
|
|
281
|
+
const timeoutMs = typeof config.timeoutMs === 'number' && Number.isFinite(config.timeoutMs) && config.timeoutMs > 0
|
|
282
|
+
? Math.min(Math.floor(config.timeoutMs), 120000)
|
|
283
|
+
: undefined;
|
|
284
|
+
const legacyOnLlmError = toNonEmptyString(config.onLlmError);
|
|
285
|
+
const legacyErrorRewriteText = toNonEmptyString(config.errorRewriteText) ?? undefined;
|
|
286
|
+
return {
|
|
287
|
+
model,
|
|
288
|
+
scope,
|
|
289
|
+
rulePrompt,
|
|
290
|
+
systemPrompt: buildInternalLlmPrompt(rulePrompt),
|
|
291
|
+
outputMethod,
|
|
292
|
+
legacyOnLlmError: legacyOnLlmError ?? undefined,
|
|
293
|
+
legacyErrorRewriteText,
|
|
294
|
+
rewriteFallbackText: toNonEmptyString(config.rewriteFallbackText) ?? DEFAULT_REWRITE_TEXT,
|
|
295
|
+
timeoutMs,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function withTimeout(promise, timeoutMs) {
|
|
299
|
+
if (!timeoutMs || timeoutMs <= 0) {
|
|
300
|
+
return promise;
|
|
301
|
+
}
|
|
302
|
+
return new Promise((resolve, reject) => {
|
|
303
|
+
const timer = setTimeout(() => {
|
|
304
|
+
reject(new Error(`LLM filter timeout after ${timeoutMs}ms`));
|
|
305
|
+
}, timeoutMs);
|
|
306
|
+
promise
|
|
307
|
+
.then((result) => {
|
|
308
|
+
clearTimeout(timer);
|
|
309
|
+
resolve(result);
|
|
310
|
+
})
|
|
311
|
+
.catch((error) => {
|
|
312
|
+
clearTimeout(timer);
|
|
313
|
+
reject(error);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
function normalizeConfigurable(input) {
|
|
318
|
+
if (!isRecord(input)) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
return input;
|
|
322
|
+
}
|
|
323
|
+
function buildOutputMethodCandidates(preferred) {
|
|
324
|
+
const queue = [preferred, 'functionCalling', 'jsonMode', 'jsonSchema'];
|
|
325
|
+
const unique = [];
|
|
326
|
+
for (const method of queue) {
|
|
327
|
+
if (!unique.includes(method)) {
|
|
328
|
+
unique.push(method);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return unique;
|
|
332
|
+
}
|
|
333
|
+
function getErrorText(error) {
|
|
334
|
+
if (error instanceof Error) {
|
|
335
|
+
return error.message;
|
|
336
|
+
}
|
|
337
|
+
return String(error ?? '');
|
|
338
|
+
}
|
|
339
|
+
function isUnsupportedStructuredOutputError(error) {
|
|
340
|
+
const message = getErrorText(error).toLowerCase();
|
|
341
|
+
const patterns = [
|
|
342
|
+
'response_format type is unavailable',
|
|
343
|
+
'invalid response_format',
|
|
344
|
+
'response_format',
|
|
345
|
+
'unsupported schema',
|
|
346
|
+
'not support',
|
|
347
|
+
];
|
|
348
|
+
return patterns.some((pattern) => message.includes(pattern));
|
|
349
|
+
}
|
|
218
350
|
let SensitiveFilterMiddleware = class SensitiveFilterMiddleware {
|
|
219
351
|
constructor() {
|
|
220
352
|
this.meta = {
|
|
@@ -224,8 +356,8 @@ let SensitiveFilterMiddleware = class SensitiveFilterMiddleware {
|
|
|
224
356
|
zh_Hans: '敏感内容过滤中间件',
|
|
225
357
|
},
|
|
226
358
|
description: {
|
|
227
|
-
en_US: 'Filter sensitive content before input and after output using
|
|
228
|
-
zh_Hans: '
|
|
359
|
+
en_US: 'Filter sensitive content before input and after output using rule mode or LLM prompt mode (mutually exclusive).',
|
|
360
|
+
zh_Hans: '支持规则模式或 LLM 提示词模式(互斥)进行输入/输出敏感内容过滤。',
|
|
229
361
|
},
|
|
230
362
|
icon: {
|
|
231
363
|
type: 'svg',
|
|
@@ -234,15 +366,37 @@ let SensitiveFilterMiddleware = class SensitiveFilterMiddleware {
|
|
|
234
366
|
configSchema: {
|
|
235
367
|
type: 'object',
|
|
236
368
|
properties: {
|
|
369
|
+
mode: {
|
|
370
|
+
type: 'string',
|
|
371
|
+
title: {
|
|
372
|
+
en_US: 'Filter Mode',
|
|
373
|
+
zh_Hans: '过滤模式',
|
|
374
|
+
},
|
|
375
|
+
description: {
|
|
376
|
+
en_US: 'Choose exactly one mode: Rule or LLM.',
|
|
377
|
+
zh_Hans: '二选一:规则模式或 LLM 模式。',
|
|
378
|
+
},
|
|
379
|
+
enum: ['rule', 'llm'],
|
|
380
|
+
default: 'rule',
|
|
381
|
+
'x-ui': {
|
|
382
|
+
enumLabels: {
|
|
383
|
+
rule: { en_US: 'Rule Mode', zh_Hans: '规则模式' },
|
|
384
|
+
llm: { en_US: 'LLM Mode', zh_Hans: 'LLM 模式' },
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
},
|
|
237
388
|
rules: {
|
|
238
389
|
type: 'array',
|
|
390
|
+
'x-ui': {
|
|
391
|
+
span: 2,
|
|
392
|
+
},
|
|
239
393
|
title: {
|
|
240
394
|
en_US: 'Business Rules',
|
|
241
395
|
zh_Hans: '业务规则',
|
|
242
396
|
},
|
|
243
397
|
description: {
|
|
244
|
-
en_US: '
|
|
245
|
-
zh_Hans: '
|
|
398
|
+
en_US: 'Used in rule mode. Draft rows are allowed during editing. Runtime requires valid fields.',
|
|
399
|
+
zh_Hans: '规则模式使用。编辑阶段允许草稿行,运行阶段要求有效规则字段。',
|
|
246
400
|
},
|
|
247
401
|
items: {
|
|
248
402
|
type: 'object',
|
|
@@ -306,29 +460,17 @@ let SensitiveFilterMiddleware = class SensitiveFilterMiddleware {
|
|
|
306
460
|
},
|
|
307
461
|
replacementText: {
|
|
308
462
|
type: 'string',
|
|
309
|
-
title: { en_US: 'Replacement Text', zh_Hans: '
|
|
463
|
+
title: { en_US: 'Replacement Text', zh_Hans: '替换文本(可选)' },
|
|
310
464
|
},
|
|
311
465
|
},
|
|
312
|
-
required: ['pattern', 'type', 'action'],
|
|
313
|
-
allOf: [
|
|
314
|
-
{
|
|
315
|
-
if: {
|
|
316
|
-
properties: {
|
|
317
|
-
action: {
|
|
318
|
-
const: 'rewrite',
|
|
319
|
-
},
|
|
320
|
-
},
|
|
321
|
-
},
|
|
322
|
-
then: {
|
|
323
|
-
required: ['replacementText'],
|
|
324
|
-
},
|
|
325
|
-
},
|
|
326
|
-
],
|
|
466
|
+
required: ['pattern', 'type', 'action', 'scope', 'severity'],
|
|
327
467
|
},
|
|
328
|
-
minItems: 1,
|
|
329
468
|
},
|
|
330
469
|
generalPack: {
|
|
331
470
|
type: 'object',
|
|
471
|
+
'x-ui': {
|
|
472
|
+
span: 2,
|
|
473
|
+
},
|
|
332
474
|
title: {
|
|
333
475
|
en_US: 'General Pack',
|
|
334
476
|
zh_Hans: '通用规则包(本地开源词库)',
|
|
@@ -346,6 +488,10 @@ let SensitiveFilterMiddleware = class SensitiveFilterMiddleware {
|
|
|
346
488
|
profile: {
|
|
347
489
|
type: 'string',
|
|
348
490
|
title: { en_US: 'Profile', zh_Hans: '策略档位' },
|
|
491
|
+
description: {
|
|
492
|
+
en_US: 'Strict blocks on hit with broader lexicon; Balanced rewrites on hit with smaller lexicon.',
|
|
493
|
+
zh_Hans: '严格:词库范围更大且命中后拦截;平衡:词库范围较小且命中后改写。',
|
|
494
|
+
},
|
|
349
495
|
enum: ['strict', 'balanced'],
|
|
350
496
|
'x-ui': {
|
|
351
497
|
enumLabels: {
|
|
@@ -365,80 +511,283 @@ let SensitiveFilterMiddleware = class SensitiveFilterMiddleware {
|
|
|
365
511
|
type: 'boolean',
|
|
366
512
|
default: false,
|
|
367
513
|
title: { en_US: 'Case Sensitive', zh_Hans: '区分大小写' },
|
|
514
|
+
'x-ui': {
|
|
515
|
+
span: 2,
|
|
516
|
+
},
|
|
368
517
|
},
|
|
369
518
|
normalize: {
|
|
370
519
|
type: 'boolean',
|
|
371
520
|
default: true,
|
|
372
521
|
title: { en_US: 'Normalize Text', zh_Hans: '文本标准化' },
|
|
522
|
+
'x-ui': {
|
|
523
|
+
span: 2,
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
llm: {
|
|
527
|
+
type: 'object',
|
|
528
|
+
'x-ui': {
|
|
529
|
+
span: 2,
|
|
530
|
+
},
|
|
531
|
+
title: {
|
|
532
|
+
en_US: 'LLM Filter Config',
|
|
533
|
+
zh_Hans: 'LLM 过滤配置',
|
|
534
|
+
},
|
|
535
|
+
description: {
|
|
536
|
+
en_US: 'Used only when mode=llm.',
|
|
537
|
+
zh_Hans: '仅在 mode=llm 时生效。',
|
|
538
|
+
},
|
|
539
|
+
properties: {
|
|
540
|
+
model: {
|
|
541
|
+
type: 'object',
|
|
542
|
+
title: {
|
|
543
|
+
en_US: 'Filter Model',
|
|
544
|
+
zh_Hans: '过滤模型',
|
|
545
|
+
},
|
|
546
|
+
'x-ui': {
|
|
547
|
+
component: 'ai-model-select',
|
|
548
|
+
span: 2,
|
|
549
|
+
inputs: {
|
|
550
|
+
modelType: 'llm',
|
|
551
|
+
hiddenLabel: true,
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
scope: {
|
|
556
|
+
type: 'string',
|
|
557
|
+
title: { en_US: 'Scope', zh_Hans: '生效范围' },
|
|
558
|
+
enum: ['input', 'output', 'both'],
|
|
559
|
+
'x-ui': {
|
|
560
|
+
enumLabels: {
|
|
561
|
+
input: { en_US: 'Input', zh_Hans: '仅输入' },
|
|
562
|
+
output: { en_US: 'Output', zh_Hans: '仅输出' },
|
|
563
|
+
both: { en_US: 'Both', zh_Hans: '输入和输出' },
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
rulePrompt: {
|
|
568
|
+
type: 'string',
|
|
569
|
+
title: { en_US: 'Rule Prompt', zh_Hans: '审核规则说明' },
|
|
570
|
+
description: {
|
|
571
|
+
en_US: 'Describe your moderation rules in natural language. No JSON format is required.',
|
|
572
|
+
zh_Hans: '用自然语言描述审核规则,无需手写 JSON 格式。',
|
|
573
|
+
},
|
|
574
|
+
'x-ui': {
|
|
575
|
+
component: 'textarea',
|
|
576
|
+
span: 2,
|
|
577
|
+
placeholder: {
|
|
578
|
+
en_US: 'e.g. Rewrite violent/privacy-sensitive content into a safe neutral response.',
|
|
579
|
+
zh_Hans: '例如:涉及暴力或隐私泄露内容时,改写为安全中性表达。',
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
outputMethod: {
|
|
584
|
+
type: 'string',
|
|
585
|
+
title: { en_US: 'Output Method', zh_Hans: '结构化输出方式' },
|
|
586
|
+
enum: ['functionCalling', 'jsonMode', 'jsonSchema'],
|
|
587
|
+
default: 'jsonMode',
|
|
588
|
+
'x-ui': {
|
|
589
|
+
enumLabels: {
|
|
590
|
+
functionCalling: { en_US: 'Function Calling', zh_Hans: '函数调用' },
|
|
591
|
+
jsonMode: { en_US: 'JSON Mode', zh_Hans: 'JSON模式' },
|
|
592
|
+
jsonSchema: { en_US: 'JSON Schema', zh_Hans: 'JSON架构' },
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
rewriteFallbackText: {
|
|
597
|
+
type: 'string',
|
|
598
|
+
title: { en_US: 'Rewrite Fallback Text', zh_Hans: '改写兜底文本' },
|
|
599
|
+
},
|
|
600
|
+
timeoutMs: {
|
|
601
|
+
type: 'number',
|
|
602
|
+
title: { en_US: 'Timeout (ms)', zh_Hans: '超时毫秒' },
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
required: ['model', 'scope', 'rulePrompt'],
|
|
373
606
|
},
|
|
374
607
|
},
|
|
608
|
+
required: ['mode'],
|
|
609
|
+
allOf: [
|
|
610
|
+
{
|
|
611
|
+
if: {
|
|
612
|
+
properties: {
|
|
613
|
+
mode: {
|
|
614
|
+
const: 'llm',
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
then: {
|
|
619
|
+
required: ['llm'],
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
],
|
|
375
623
|
},
|
|
376
624
|
};
|
|
377
625
|
}
|
|
378
|
-
createMiddleware(options,
|
|
626
|
+
async createMiddleware(options, context) {
|
|
379
627
|
const parsed = sensitiveFilterConfigSchema.safeParse(options ?? {});
|
|
380
628
|
if (!parsed.success) {
|
|
381
|
-
throw new Error(
|
|
629
|
+
throw new Error(CONFIG_PARSE_ERROR);
|
|
630
|
+
}
|
|
631
|
+
if (parsed.data.mode === 'llm') {
|
|
632
|
+
return this.createLlmModeMiddleware(parsed.data, context);
|
|
382
633
|
}
|
|
383
|
-
|
|
634
|
+
return this.createRuleModeMiddleware(parsed.data, context);
|
|
635
|
+
}
|
|
636
|
+
createRuleModeMiddleware(config, context) {
|
|
384
637
|
const caseSensitive = config.caseSensitive ?? false;
|
|
385
638
|
const normalize = config.normalize ?? true;
|
|
386
|
-
const customRules = normalizeRuleDrafts(config.rules);
|
|
639
|
+
const customRules = normalizeRuleDrafts(config.rules ?? []);
|
|
387
640
|
const generalRules = resolveGeneralPackRules(config.generalPack);
|
|
388
641
|
const allRules = [...customRules, ...generalRules];
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
642
|
+
const hasEffectiveRules = allRules.length > 0;
|
|
643
|
+
let compiledRulesCache = null;
|
|
644
|
+
const getCompiledRules = () => {
|
|
645
|
+
if (compiledRulesCache) {
|
|
646
|
+
return compiledRulesCache;
|
|
647
|
+
}
|
|
648
|
+
compiledRulesCache = allRules.map((rule, index) => {
|
|
649
|
+
const normalizedPattern = rule.type === 'keyword' ? normalizeForMatching(rule.pattern, normalize, caseSensitive) : rule.pattern;
|
|
650
|
+
if (rule.type === 'regex') {
|
|
651
|
+
try {
|
|
652
|
+
return {
|
|
653
|
+
...rule,
|
|
654
|
+
index,
|
|
655
|
+
normalizedPattern,
|
|
656
|
+
matchRegex: new RegExp(rule.pattern, caseSensitive ? '' : 'i'),
|
|
657
|
+
rewriteRegex: new RegExp(rule.pattern, caseSensitive ? 'g' : 'gi'),
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
catch (error) {
|
|
661
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
662
|
+
throw new Error(`请完善规则配置:规则「${rule.id}」的正则表达式不合法(${message})。`);
|
|
663
|
+
}
|
|
404
664
|
}
|
|
665
|
+
return {
|
|
666
|
+
...rule,
|
|
667
|
+
index,
|
|
668
|
+
normalizedPattern,
|
|
669
|
+
};
|
|
670
|
+
});
|
|
671
|
+
return compiledRulesCache;
|
|
672
|
+
};
|
|
673
|
+
let inputBlockedMessage = null;
|
|
674
|
+
let pendingInputRewrite = null;
|
|
675
|
+
let finalAction = 'pass';
|
|
676
|
+
let auditEntries = [];
|
|
677
|
+
let runtimeConfigurable = null;
|
|
678
|
+
const resetRunState = () => {
|
|
679
|
+
inputBlockedMessage = null;
|
|
680
|
+
pendingInputRewrite = null;
|
|
681
|
+
finalAction = 'pass';
|
|
682
|
+
auditEntries = [];
|
|
683
|
+
};
|
|
684
|
+
const pushAudit = (entry) => {
|
|
685
|
+
auditEntries.push({
|
|
686
|
+
...entry,
|
|
687
|
+
timestamp: new Date().toISOString(),
|
|
688
|
+
mode: 'rule',
|
|
689
|
+
});
|
|
690
|
+
};
|
|
691
|
+
const assignRuntimeConfigurable = (runtimeLike) => {
|
|
692
|
+
const configurable = normalizeConfigurable(runtimeLike?.configurable);
|
|
693
|
+
if (!configurable) {
|
|
694
|
+
return;
|
|
405
695
|
}
|
|
696
|
+
if (configurable.thread_id && configurable.executionId) {
|
|
697
|
+
runtimeConfigurable = configurable;
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
const buildAuditSnapshot = () => {
|
|
701
|
+
const summary = {
|
|
702
|
+
total: auditEntries.length,
|
|
703
|
+
matched: auditEntries.filter((entry) => entry.matched).length,
|
|
704
|
+
blocked: auditEntries.filter((entry) => entry.action === 'block').length,
|
|
705
|
+
rewritten: auditEntries.filter((entry) => entry.action === 'rewrite').length,
|
|
706
|
+
errorPolicyTriggered: auditEntries.filter((entry) => entry.errorPolicyTriggered).length,
|
|
707
|
+
};
|
|
406
708
|
return {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
709
|
+
mode: 'rule',
|
|
710
|
+
finalAction,
|
|
711
|
+
records: auditEntries,
|
|
712
|
+
summary,
|
|
410
713
|
};
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
|
|
714
|
+
};
|
|
715
|
+
const persistAuditSnapshot = async () => {
|
|
716
|
+
const configurable = runtimeConfigurable;
|
|
717
|
+
if (!configurable?.thread_id || !configurable.executionId || !this.commandBus) {
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
const { thread_id, checkpoint_ns, checkpoint_id, subscriber, executionId } = configurable;
|
|
721
|
+
const snapshot = buildAuditSnapshot();
|
|
722
|
+
await this.commandBus.execute(new WrapWorkflowNodeExecutionCommand(async () => {
|
|
723
|
+
return {
|
|
724
|
+
state: snapshot,
|
|
725
|
+
output: snapshot,
|
|
726
|
+
};
|
|
727
|
+
}, {
|
|
728
|
+
execution: {
|
|
729
|
+
category: 'workflow',
|
|
730
|
+
type: 'middleware',
|
|
731
|
+
title: `${context.node.title} Audit`,
|
|
732
|
+
inputs: {
|
|
733
|
+
mode: snapshot.mode,
|
|
734
|
+
total: snapshot.summary.total,
|
|
735
|
+
},
|
|
736
|
+
parentId: executionId,
|
|
737
|
+
threadId: thread_id,
|
|
738
|
+
checkpointNs: checkpoint_ns,
|
|
739
|
+
checkpointId: checkpoint_id,
|
|
740
|
+
agentKey: context.node.key,
|
|
741
|
+
},
|
|
742
|
+
subscriber,
|
|
743
|
+
}));
|
|
744
|
+
};
|
|
414
745
|
return {
|
|
415
746
|
name: SENSITIVE_FILTER_MIDDLEWARE_NAME,
|
|
416
747
|
beforeAgent: async (state, runtime) => {
|
|
748
|
+
resetRunState();
|
|
749
|
+
assignRuntimeConfigurable(runtime);
|
|
750
|
+
if (!hasEffectiveRules) {
|
|
751
|
+
throw new Error(BUSINESS_RULES_VALIDATION_ERROR);
|
|
752
|
+
}
|
|
753
|
+
const compiledRules = getCompiledRules();
|
|
417
754
|
const safeState = state ?? {};
|
|
418
755
|
const safeRuntime = runtime ?? {};
|
|
419
|
-
inputBlockedMessage = null;
|
|
420
|
-
pendingInputRewrite = null;
|
|
421
|
-
if (compiledRules.length === 0) {
|
|
422
|
-
return undefined;
|
|
423
|
-
}
|
|
424
756
|
const inputText = extractInputText(safeState, safeRuntime);
|
|
425
757
|
const inputMatches = findMatches(inputText, 'input', compiledRules, normalize, caseSensitive);
|
|
426
758
|
const winner = pickWinningRule(inputMatches);
|
|
427
759
|
if (!winner) {
|
|
760
|
+
pushAudit({
|
|
761
|
+
phase: 'input',
|
|
762
|
+
matched: false,
|
|
763
|
+
source: 'rule',
|
|
764
|
+
errorPolicyTriggered: false,
|
|
765
|
+
});
|
|
428
766
|
return undefined;
|
|
429
767
|
}
|
|
768
|
+
pushAudit({
|
|
769
|
+
phase: 'input',
|
|
770
|
+
matched: true,
|
|
771
|
+
source: 'rule',
|
|
772
|
+
action: winner.action,
|
|
773
|
+
reason: `rule:${winner.id}`,
|
|
774
|
+
errorPolicyTriggered: false,
|
|
775
|
+
});
|
|
430
776
|
if (winner.action === 'block') {
|
|
777
|
+
finalAction = 'block';
|
|
431
778
|
inputBlockedMessage = winner.replacementText?.trim() || DEFAULT_INPUT_BLOCK_MESSAGE;
|
|
432
779
|
return undefined;
|
|
433
780
|
}
|
|
434
|
-
|
|
435
|
-
pendingInputRewrite =
|
|
781
|
+
finalAction = 'rewrite';
|
|
782
|
+
pendingInputRewrite = rewriteTextByRule(inputText, winner, caseSensitive);
|
|
436
783
|
return undefined;
|
|
437
784
|
},
|
|
438
785
|
wrapModelCall: async (request, handler) => {
|
|
439
|
-
|
|
440
|
-
|
|
786
|
+
assignRuntimeConfigurable(request?.runtime);
|
|
787
|
+
if (!hasEffectiveRules) {
|
|
788
|
+
throw new Error(BUSINESS_RULES_VALIDATION_ERROR);
|
|
441
789
|
}
|
|
790
|
+
const compiledRules = getCompiledRules();
|
|
442
791
|
if (inputBlockedMessage) {
|
|
443
792
|
return new AIMessage(inputBlockedMessage);
|
|
444
793
|
}
|
|
@@ -449,21 +798,371 @@ let SensitiveFilterMiddleware = class SensitiveFilterMiddleware {
|
|
|
449
798
|
const outputMatches = findMatches(outputText, 'output', compiledRules, normalize, caseSensitive);
|
|
450
799
|
const winner = pickWinningRule(outputMatches);
|
|
451
800
|
if (!winner) {
|
|
801
|
+
pushAudit({
|
|
802
|
+
phase: 'output',
|
|
803
|
+
matched: false,
|
|
804
|
+
source: 'rule',
|
|
805
|
+
errorPolicyTriggered: false,
|
|
806
|
+
});
|
|
452
807
|
return response;
|
|
453
808
|
}
|
|
809
|
+
pushAudit({
|
|
810
|
+
phase: 'output',
|
|
811
|
+
matched: true,
|
|
812
|
+
source: 'rule',
|
|
813
|
+
action: winner.action,
|
|
814
|
+
reason: `rule:${winner.id}`,
|
|
815
|
+
errorPolicyTriggered: false,
|
|
816
|
+
});
|
|
454
817
|
if (winner.action === 'block') {
|
|
818
|
+
finalAction = 'block';
|
|
455
819
|
const blockedOutput = winner.replacementText?.trim() || DEFAULT_OUTPUT_BLOCK_MESSAGE;
|
|
456
820
|
return replaceModelResponseText(response, blockedOutput);
|
|
457
821
|
}
|
|
822
|
+
finalAction = 'rewrite';
|
|
458
823
|
const rewrittenOutput = rewriteTextByRule(outputText, winner, caseSensitive);
|
|
459
824
|
return replaceModelResponseText(response, rewrittenOutput);
|
|
460
825
|
},
|
|
461
826
|
afterAgent: async () => {
|
|
827
|
+
await persistAuditSnapshot();
|
|
828
|
+
return undefined;
|
|
829
|
+
},
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
createLlmModeMiddleware(config, context) {
|
|
833
|
+
const llmDraftConfig = config.llm;
|
|
834
|
+
let resolvedLlmConfig = null;
|
|
835
|
+
let modelPromise = null;
|
|
836
|
+
const structuredModelPromises = new Map();
|
|
837
|
+
const getLlmConfig = () => {
|
|
838
|
+
if (!resolvedLlmConfig) {
|
|
839
|
+
resolvedLlmConfig = resolveRuntimeLlmConfig(llmDraftConfig);
|
|
840
|
+
}
|
|
841
|
+
return resolvedLlmConfig;
|
|
842
|
+
};
|
|
843
|
+
const ensureModel = async () => {
|
|
844
|
+
const llmConfig = getLlmConfig();
|
|
845
|
+
if (!modelPromise) {
|
|
846
|
+
modelPromise = this.commandBus.execute(new CreateModelClientCommand(llmConfig.model, {
|
|
847
|
+
usageCallback: () => { },
|
|
848
|
+
}));
|
|
849
|
+
}
|
|
850
|
+
return modelPromise;
|
|
851
|
+
};
|
|
852
|
+
const ensureStructuredModel = async (method) => {
|
|
853
|
+
if (!structuredModelPromises.has(method)) {
|
|
854
|
+
structuredModelPromises.set(method, (async () => {
|
|
855
|
+
const model = await ensureModel();
|
|
856
|
+
return model.withStructuredOutput?.(llmDecisionSchema, {
|
|
857
|
+
method,
|
|
858
|
+
}) ?? null;
|
|
859
|
+
})());
|
|
860
|
+
}
|
|
861
|
+
return structuredModelPromises.get(method);
|
|
862
|
+
};
|
|
863
|
+
let pendingInputRewrite = null;
|
|
864
|
+
let finalAction = 'pass';
|
|
865
|
+
let auditEntries = [];
|
|
866
|
+
let runtimeConfigurable = null;
|
|
867
|
+
let resolvedOutputMethod;
|
|
868
|
+
let fallbackTriggered = false;
|
|
869
|
+
let methodAttempts = [];
|
|
870
|
+
const resetRunState = () => {
|
|
871
|
+
pendingInputRewrite = null;
|
|
872
|
+
finalAction = 'pass';
|
|
873
|
+
auditEntries = [];
|
|
874
|
+
resolvedOutputMethod = undefined;
|
|
875
|
+
fallbackTriggered = false;
|
|
876
|
+
methodAttempts = [];
|
|
877
|
+
};
|
|
878
|
+
const pushAudit = (entry) => {
|
|
879
|
+
auditEntries.push({
|
|
880
|
+
...entry,
|
|
881
|
+
timestamp: new Date().toISOString(),
|
|
882
|
+
mode: 'llm',
|
|
883
|
+
});
|
|
884
|
+
};
|
|
885
|
+
const assignRuntimeConfigurable = (runtimeLike) => {
|
|
886
|
+
const configurable = normalizeConfigurable(runtimeLike?.configurable);
|
|
887
|
+
if (!configurable) {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (configurable.thread_id && configurable.executionId) {
|
|
891
|
+
runtimeConfigurable = configurable;
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
const captureLlmOutputTrace = (trace) => {
|
|
895
|
+
for (const method of trace.methodAttempts) {
|
|
896
|
+
if (!methodAttempts.includes(method)) {
|
|
897
|
+
methodAttempts.push(method);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
resolvedOutputMethod = trace.resolvedOutputMethod;
|
|
901
|
+
fallbackTriggered = fallbackTriggered || trace.fallbackTriggered;
|
|
902
|
+
};
|
|
903
|
+
const buildAuditSnapshot = () => {
|
|
904
|
+
const summary = {
|
|
905
|
+
total: auditEntries.length,
|
|
906
|
+
matched: auditEntries.filter((entry) => entry.matched).length,
|
|
907
|
+
blocked: auditEntries.filter((entry) => entry.action === 'block').length,
|
|
908
|
+
rewritten: auditEntries.filter((entry) => entry.action === 'rewrite').length,
|
|
909
|
+
errorPolicyTriggered: auditEntries.filter((entry) => entry.errorPolicyTriggered).length,
|
|
910
|
+
};
|
|
911
|
+
return {
|
|
912
|
+
mode: 'llm',
|
|
913
|
+
finalAction,
|
|
914
|
+
records: auditEntries,
|
|
915
|
+
summary,
|
|
916
|
+
llmOutput: resolvedLlmConfig
|
|
917
|
+
? {
|
|
918
|
+
requestedOutputMethod: resolvedLlmConfig.outputMethod,
|
|
919
|
+
resolvedOutputMethod,
|
|
920
|
+
methodAttempts,
|
|
921
|
+
fallbackTriggered,
|
|
922
|
+
}
|
|
923
|
+
: undefined,
|
|
924
|
+
};
|
|
925
|
+
};
|
|
926
|
+
const persistAuditSnapshot = async () => {
|
|
927
|
+
const configurable = runtimeConfigurable;
|
|
928
|
+
if (!configurable?.thread_id || !configurable.executionId || !this.commandBus) {
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
const { thread_id, checkpoint_ns, checkpoint_id, subscriber, executionId } = configurable;
|
|
932
|
+
const snapshot = buildAuditSnapshot();
|
|
933
|
+
await this.commandBus.execute(new WrapWorkflowNodeExecutionCommand(async () => {
|
|
934
|
+
return {
|
|
935
|
+
state: snapshot,
|
|
936
|
+
output: snapshot,
|
|
937
|
+
};
|
|
938
|
+
}, {
|
|
939
|
+
execution: {
|
|
940
|
+
category: 'workflow',
|
|
941
|
+
type: 'middleware',
|
|
942
|
+
title: `${context.node.title} Audit`,
|
|
943
|
+
inputs: {
|
|
944
|
+
mode: snapshot.mode,
|
|
945
|
+
total: snapshot.summary.total,
|
|
946
|
+
},
|
|
947
|
+
parentId: executionId,
|
|
948
|
+
threadId: thread_id,
|
|
949
|
+
checkpointNs: checkpoint_ns,
|
|
950
|
+
checkpointId: checkpoint_id,
|
|
951
|
+
agentKey: context.node.key,
|
|
952
|
+
},
|
|
953
|
+
subscriber,
|
|
954
|
+
}));
|
|
955
|
+
};
|
|
956
|
+
const buildEvaluationMessages = (phase, text, llmConfig) => {
|
|
957
|
+
return [
|
|
958
|
+
{ role: 'system', content: llmConfig.systemPrompt },
|
|
959
|
+
{
|
|
960
|
+
role: 'user',
|
|
961
|
+
content: `phase=${phase}\n` +
|
|
962
|
+
'请严格基于给定文本进行敏感判定,并只返回约定结构。\n' +
|
|
963
|
+
`text:\n${text}`,
|
|
964
|
+
},
|
|
965
|
+
];
|
|
966
|
+
};
|
|
967
|
+
const invokeAndTrack = async (phase, text, runtime, llmConfig) => {
|
|
968
|
+
const invokeCore = async () => {
|
|
969
|
+
const messages = buildEvaluationMessages(phase, text, llmConfig);
|
|
970
|
+
const model = await ensureModel();
|
|
971
|
+
const candidates = buildOutputMethodCandidates(llmConfig.outputMethod);
|
|
972
|
+
const attempts = [];
|
|
973
|
+
for (const method of candidates) {
|
|
974
|
+
attempts.push(method);
|
|
975
|
+
try {
|
|
976
|
+
const structuredModel = await ensureStructuredModel(method);
|
|
977
|
+
if (!structuredModel) {
|
|
978
|
+
throw new Error(`Structured output is not available for method: ${method}`);
|
|
979
|
+
}
|
|
980
|
+
const raw = await withTimeout(structuredModel.invoke(messages), llmConfig.timeoutMs);
|
|
981
|
+
return {
|
|
982
|
+
raw,
|
|
983
|
+
trace: {
|
|
984
|
+
requestedOutputMethod: llmConfig.outputMethod,
|
|
985
|
+
resolvedOutputMethod: method,
|
|
986
|
+
methodAttempts: attempts,
|
|
987
|
+
fallbackTriggered: method !== llmConfig.outputMethod || attempts.length > 1,
|
|
988
|
+
},
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
catch (error) {
|
|
992
|
+
if (isUnsupportedStructuredOutputError(error)) {
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
throw error;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
attempts.push('plainText');
|
|
999
|
+
const raw = await withTimeout(model.invoke(messages), llmConfig.timeoutMs);
|
|
1000
|
+
return {
|
|
1001
|
+
raw,
|
|
1002
|
+
trace: {
|
|
1003
|
+
requestedOutputMethod: llmConfig.outputMethod,
|
|
1004
|
+
resolvedOutputMethod: 'plainText',
|
|
1005
|
+
methodAttempts: attempts,
|
|
1006
|
+
fallbackTriggered: true,
|
|
1007
|
+
},
|
|
1008
|
+
};
|
|
1009
|
+
};
|
|
1010
|
+
const parseCore = async () => {
|
|
1011
|
+
const { raw, trace } = await invokeCore();
|
|
1012
|
+
captureLlmOutputTrace(trace);
|
|
1013
|
+
return parseLlmDecision(raw, llmConfig.rewriteFallbackText);
|
|
1014
|
+
};
|
|
1015
|
+
const configurable = (runtime?.configurable ?? {});
|
|
1016
|
+
const { thread_id, checkpoint_ns, checkpoint_id, subscriber, executionId } = configurable;
|
|
1017
|
+
if (!thread_id || !executionId) {
|
|
1018
|
+
return parseCore();
|
|
1019
|
+
}
|
|
1020
|
+
const tracked = await this.commandBus.execute(new WrapWorkflowNodeExecutionCommand(async () => {
|
|
1021
|
+
const decision = await parseCore();
|
|
1022
|
+
return {
|
|
1023
|
+
state: decision,
|
|
1024
|
+
output: decision,
|
|
1025
|
+
};
|
|
1026
|
+
}, {
|
|
1027
|
+
execution: {
|
|
1028
|
+
category: 'workflow',
|
|
1029
|
+
type: 'middleware',
|
|
1030
|
+
inputs: {
|
|
1031
|
+
phase,
|
|
1032
|
+
text,
|
|
1033
|
+
},
|
|
1034
|
+
parentId: executionId,
|
|
1035
|
+
threadId: thread_id,
|
|
1036
|
+
checkpointNs: checkpoint_ns,
|
|
1037
|
+
checkpointId: checkpoint_id,
|
|
1038
|
+
agentKey: context.node.key,
|
|
1039
|
+
title: context.node.title,
|
|
1040
|
+
},
|
|
1041
|
+
subscriber,
|
|
1042
|
+
}));
|
|
1043
|
+
return tracked;
|
|
1044
|
+
};
|
|
1045
|
+
const resolveOnErrorDecision = (llmConfig, error) => {
|
|
1046
|
+
const reason = `llm-error:${error instanceof Error ? error.message : String(error)}`;
|
|
1047
|
+
return {
|
|
1048
|
+
matched: true,
|
|
1049
|
+
action: 'rewrite',
|
|
1050
|
+
replacementText: llmConfig.legacyErrorRewriteText ?? llmConfig.rewriteFallbackText,
|
|
1051
|
+
reason,
|
|
1052
|
+
};
|
|
1053
|
+
};
|
|
1054
|
+
return {
|
|
1055
|
+
name: SENSITIVE_FILTER_MIDDLEWARE_NAME,
|
|
1056
|
+
beforeAgent: async (state, runtime) => {
|
|
1057
|
+
resetRunState();
|
|
1058
|
+
assignRuntimeConfigurable(runtime);
|
|
1059
|
+
const llmConfig = getLlmConfig();
|
|
1060
|
+
if (!modeIncludesScope(llmConfig.scope, 'input')) {
|
|
1061
|
+
pushAudit({
|
|
1062
|
+
phase: 'input',
|
|
1063
|
+
matched: false,
|
|
1064
|
+
source: 'llm',
|
|
1065
|
+
reason: 'scope-skip',
|
|
1066
|
+
errorPolicyTriggered: false,
|
|
1067
|
+
});
|
|
1068
|
+
return undefined;
|
|
1069
|
+
}
|
|
1070
|
+
const inputText = extractInputText(state ?? {}, runtime ?? {});
|
|
1071
|
+
if (!inputText) {
|
|
1072
|
+
pushAudit({
|
|
1073
|
+
phase: 'input',
|
|
1074
|
+
matched: false,
|
|
1075
|
+
source: 'llm',
|
|
1076
|
+
reason: 'empty-input',
|
|
1077
|
+
errorPolicyTriggered: false,
|
|
1078
|
+
});
|
|
1079
|
+
return undefined;
|
|
1080
|
+
}
|
|
1081
|
+
let decision;
|
|
1082
|
+
let fromErrorPolicy = false;
|
|
1083
|
+
try {
|
|
1084
|
+
decision = await invokeAndTrack('input', inputText, runtime, llmConfig);
|
|
1085
|
+
}
|
|
1086
|
+
catch (error) {
|
|
1087
|
+
decision = resolveOnErrorDecision(llmConfig, error);
|
|
1088
|
+
fromErrorPolicy = true;
|
|
1089
|
+
}
|
|
1090
|
+
pushAudit({
|
|
1091
|
+
phase: 'input',
|
|
1092
|
+
matched: decision.matched,
|
|
1093
|
+
source: fromErrorPolicy ? 'error-policy' : 'llm',
|
|
1094
|
+
action: decision.action,
|
|
1095
|
+
reason: decision.reason,
|
|
1096
|
+
errorPolicyTriggered: fromErrorPolicy,
|
|
1097
|
+
});
|
|
1098
|
+
if (!decision.matched || !decision.action) {
|
|
1099
|
+
return undefined;
|
|
1100
|
+
}
|
|
1101
|
+
finalAction = 'rewrite';
|
|
1102
|
+
pendingInputRewrite = toNonEmptyString(decision.replacementText) ?? llmConfig.rewriteFallbackText;
|
|
1103
|
+
return undefined;
|
|
1104
|
+
},
|
|
1105
|
+
wrapModelCall: async (request, handler) => {
|
|
1106
|
+
assignRuntimeConfigurable(request?.runtime);
|
|
1107
|
+
const llmConfig = getLlmConfig();
|
|
1108
|
+
const modelRequest = pendingInputRewrite ? rewriteModelRequestInput(request, pendingInputRewrite) : request;
|
|
1109
|
+
pendingInputRewrite = null;
|
|
1110
|
+
const response = await handler(modelRequest);
|
|
1111
|
+
if (!modeIncludesScope(llmConfig.scope, 'output')) {
|
|
1112
|
+
pushAudit({
|
|
1113
|
+
phase: 'output',
|
|
1114
|
+
matched: false,
|
|
1115
|
+
source: 'llm',
|
|
1116
|
+
reason: 'scope-skip',
|
|
1117
|
+
errorPolicyTriggered: false,
|
|
1118
|
+
});
|
|
1119
|
+
return response;
|
|
1120
|
+
}
|
|
1121
|
+
const outputText = extractModelResponseText(response);
|
|
1122
|
+
if (!outputText) {
|
|
1123
|
+
pushAudit({
|
|
1124
|
+
phase: 'output',
|
|
1125
|
+
matched: false,
|
|
1126
|
+
source: 'llm',
|
|
1127
|
+
reason: 'empty-output',
|
|
1128
|
+
errorPolicyTriggered: false,
|
|
1129
|
+
});
|
|
1130
|
+
return response;
|
|
1131
|
+
}
|
|
1132
|
+
let decision;
|
|
1133
|
+
let fromErrorPolicy = false;
|
|
1134
|
+
try {
|
|
1135
|
+
decision = await invokeAndTrack('output', outputText, request?.runtime, llmConfig);
|
|
1136
|
+
}
|
|
1137
|
+
catch (error) {
|
|
1138
|
+
decision = resolveOnErrorDecision(llmConfig, error);
|
|
1139
|
+
fromErrorPolicy = true;
|
|
1140
|
+
}
|
|
1141
|
+
pushAudit({
|
|
1142
|
+
phase: 'output',
|
|
1143
|
+
matched: decision.matched,
|
|
1144
|
+
source: fromErrorPolicy ? 'error-policy' : 'llm',
|
|
1145
|
+
action: decision.action,
|
|
1146
|
+
reason: decision.reason,
|
|
1147
|
+
errorPolicyTriggered: fromErrorPolicy,
|
|
1148
|
+
});
|
|
1149
|
+
if (!decision.matched || !decision.action) {
|
|
1150
|
+
return response;
|
|
1151
|
+
}
|
|
1152
|
+
finalAction = 'rewrite';
|
|
1153
|
+
return replaceModelResponseText(response, toNonEmptyString(decision.replacementText) ?? llmConfig.rewriteFallbackText);
|
|
1154
|
+
},
|
|
1155
|
+
afterAgent: async () => {
|
|
1156
|
+
await persistAuditSnapshot();
|
|
462
1157
|
return undefined;
|
|
463
1158
|
},
|
|
464
1159
|
};
|
|
465
1160
|
}
|
|
466
1161
|
};
|
|
1162
|
+
__decorate([
|
|
1163
|
+
Inject(CommandBus),
|
|
1164
|
+
__metadata("design:type", CommandBus)
|
|
1165
|
+
], SensitiveFilterMiddleware.prototype, "commandBus", void 0);
|
|
467
1166
|
SensitiveFilterMiddleware = __decorate([
|
|
468
1167
|
Injectable(),
|
|
469
1168
|
AgentMiddlewareStrategy(SENSITIVE_FILTER_MIDDLEWARE_NAME)
|