mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
feat(Guardrails Node): Require Chat model only for LLM checks (#22241)
This commit is contained in:
@@ -1,50 +1,43 @@
|
||||
import type { IExecuteFunctions, INodeExecutionData, INodeType } from 'n8n-workflow';
|
||||
import {
|
||||
VersionedNodeType,
|
||||
type INodeTypeBaseDescription,
|
||||
type IVersionedNodeType,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { process } from './actions/process';
|
||||
import { versionDescription } from './description';
|
||||
import { getChatModel } from './helpers/model';
|
||||
import { GuardrailsV1 } from './v1/GuardrailsV1.node';
|
||||
import { GuardrailsV2 } from './v2/GuardrailsV2.node';
|
||||
|
||||
export class Guardrails implements INodeType {
|
||||
description = versionDescription;
|
||||
export class Guardrails extends VersionedNodeType {
|
||||
constructor() {
|
||||
const baseDescription: INodeTypeBaseDescription = {
|
||||
displayName: 'Guardrails',
|
||||
name: 'guardrails',
|
||||
icon: 'file:guardrails.svg',
|
||||
group: ['transform'],
|
||||
defaultVersion: 2,
|
||||
description:
|
||||
'Safeguard AI models from malicious input or prevent them from generating undesirable responses',
|
||||
codex: {
|
||||
alias: ['LangChain', 'Guardrails', 'PII', 'Secret', 'Injection', 'Sanitize'],
|
||||
categories: ['AI'],
|
||||
subcategories: {
|
||||
AI: ['Agents', 'Miscellaneous', 'Root Nodes'],
|
||||
},
|
||||
resources: {
|
||||
primaryDocumentation: [
|
||||
{
|
||||
url: 'https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-langchain.guardrails/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const operation = this.getNodeParameter('operation', 0) as 'classify' | 'sanitize';
|
||||
const model = operation === 'classify' ? await getChatModel.call(this) : null;
|
||||
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
||||
1: new GuardrailsV1(baseDescription),
|
||||
2: new GuardrailsV2(baseDescription),
|
||||
};
|
||||
|
||||
const failedItems: INodeExecutionData[] = [];
|
||||
const passedItems: INodeExecutionData[] = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
try {
|
||||
const responseData = await process.call(this, i, model);
|
||||
if (responseData.passed) {
|
||||
passedItems.push({
|
||||
json: { guardrailsInput: responseData.guardrailsInput, ...responseData.passed },
|
||||
pairedItem: { item: i },
|
||||
});
|
||||
}
|
||||
if (responseData.failed) {
|
||||
failedItems.push({
|
||||
json: { guardrailsInput: responseData.guardrailsInput, ...responseData.failed },
|
||||
pairedItem: { item: i },
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
failedItems.push({
|
||||
json: { error: error.message, guardrailsInput: '' },
|
||||
pairedItem: { item: i },
|
||||
});
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'classify') {
|
||||
return [passedItems, failedItems];
|
||||
}
|
||||
|
||||
return [passedItems];
|
||||
super(nodeVersions, baseDescription);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||
|
||||
import { process } from './process';
|
||||
import type { GuardrailsOptions } from './types';
|
||||
import { hasLLMGuardrails } from '../helpers/configureNodeInputs';
|
||||
import { getChatModel } from '../helpers/model';
|
||||
|
||||
export async function execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const operation = this.getNodeParameter('operation', 0) as 'classify' | 'sanitize';
|
||||
const model = hasLLMGuardrails(this.getNodeParameter('guardrails', 0) as GuardrailsOptions)
|
||||
? await getChatModel.call(this)
|
||||
: null;
|
||||
|
||||
const failedItems: INodeExecutionData[] = [];
|
||||
const passedItems: INodeExecutionData[] = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
try {
|
||||
const responseData = await process.call(this, i, model);
|
||||
if (responseData.passed) {
|
||||
passedItems.push({
|
||||
json: { guardrailsInput: responseData.guardrailsInput, ...responseData.passed },
|
||||
pairedItem: { item: i },
|
||||
});
|
||||
}
|
||||
if (responseData.failed) {
|
||||
failedItems.push({
|
||||
json: { guardrailsInput: responseData.guardrailsInput, ...responseData.failed },
|
||||
pairedItem: { item: i },
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
failedItems.push({
|
||||
json: { error: error.message, guardrailsInput: '' },
|
||||
pairedItem: { item: i },
|
||||
});
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'classify') {
|
||||
return [passedItems, failedItems];
|
||||
}
|
||||
|
||||
return [passedItems];
|
||||
}
|
||||
@@ -80,6 +80,13 @@ export async function process(
|
||||
input: [],
|
||||
};
|
||||
|
||||
const checkModelAvailable = (model: BaseChatModel | null): model is BaseChatModel => {
|
||||
if (!model) {
|
||||
throw new NodeOperationError(this.getNode(), 'Chat Model is required');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (guardrails.pii?.value) {
|
||||
const { entities } = guardrails.pii.value;
|
||||
stageGuardrails.preflight.push({
|
||||
@@ -121,10 +128,6 @@ export async function process(
|
||||
}
|
||||
|
||||
if (operation === 'classify') {
|
||||
if (!model) {
|
||||
throw new NodeOperationError(this.getNode(), 'Chat Model is required for classify operation');
|
||||
}
|
||||
|
||||
if (guardrails.keywords) {
|
||||
stageGuardrails.input.push({
|
||||
name: 'keywords',
|
||||
@@ -132,7 +135,7 @@ export async function process(
|
||||
});
|
||||
}
|
||||
|
||||
if (guardrails.jailbreak?.value) {
|
||||
if (guardrails.jailbreak?.value && checkModelAvailable(model)) {
|
||||
const { prompt, threshold } = guardrails.jailbreak.value;
|
||||
stageGuardrails.input.push({
|
||||
name: 'jailbreak',
|
||||
@@ -145,7 +148,7 @@ export async function process(
|
||||
});
|
||||
}
|
||||
|
||||
if (guardrails.nsfw?.value) {
|
||||
if (guardrails.nsfw?.value && checkModelAvailable(model)) {
|
||||
const { prompt, threshold } = guardrails.nsfw.value;
|
||||
stageGuardrails.input.push({
|
||||
name: 'nsfw',
|
||||
@@ -158,7 +161,7 @@ export async function process(
|
||||
});
|
||||
}
|
||||
|
||||
if (guardrails.topicalAlignment?.value) {
|
||||
if (guardrails.topicalAlignment?.value && checkModelAvailable(model)) {
|
||||
const { prompt, threshold } = guardrails.topicalAlignment.value;
|
||||
stageGuardrails.input.push({
|
||||
name: 'topicalAlignment',
|
||||
@@ -171,7 +174,7 @@ export async function process(
|
||||
});
|
||||
}
|
||||
|
||||
if (guardrails.custom?.guardrail) {
|
||||
if (guardrails.custom?.guardrail && checkModelAvailable(model)) {
|
||||
for (const customGuardrail of guardrails.custom.guardrail) {
|
||||
const { prompt, threshold, name } = customGuardrail;
|
||||
stageGuardrails.input.push({
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
|
||||
import { NodeConnectionTypes, type INodeProperties, type INodeTypeDescription } from 'n8n-workflow';
|
||||
import { type INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { JAILBREAK_PROMPT } from './actions/checks/jailbreak';
|
||||
import { NSFW_SYSTEM_PROMPT } from './actions/checks/nsfw';
|
||||
import { PII_NAME_MAP, PIIEntity } from './actions/checks/pii';
|
||||
import { TOPICAL_ALIGNMENT_SYSTEM_PROMPT } from './actions/checks/topicalAlignment';
|
||||
import { configureNodeInputs } from './helpers/configureNodeInputs';
|
||||
import { LLM_SYSTEM_RULES } from './helpers/model';
|
||||
|
||||
const THRESHOLD_OPTION: INodeProperties = {
|
||||
@@ -49,402 +48,364 @@ const wrapValue = (properties: INodeProperties[]) => ({
|
||||
values: properties,
|
||||
});
|
||||
|
||||
export const versionDescription: INodeTypeDescription = {
|
||||
displayName: 'Guardrails',
|
||||
name: 'guardrails',
|
||||
icon: 'file:guardrails.svg',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description:
|
||||
'Safeguard AI models from malicious input or prevent them from generating undesirable responses',
|
||||
defaults: {
|
||||
name: 'Guardrails',
|
||||
export const propertiesDescription: INodeProperties[] = [
|
||||
{
|
||||
displayName:
|
||||
'Use guardrails to validate text against a set of policies (e.g. NSFW, prompt injection) or to sanitize it (e.g. personal data, secret keys)',
|
||||
name: 'guardrailsUsage',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
codex: {
|
||||
alias: ['LangChain', 'Guardrails', 'PII', 'Secret', 'Injection', 'Sanitize'],
|
||||
categories: ['AI'],
|
||||
subcategories: {
|
||||
AI: ['Agents', 'Miscellaneous', 'Root Nodes'],
|
||||
},
|
||||
resources: {
|
||||
primaryDocumentation: [
|
||||
{
|
||||
url: 'https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-langchain.guardrails/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
inputs: `={{(${configureNodeInputs})($parameter.operation)}}`,
|
||||
outputs: `={{
|
||||
((parameters) => {
|
||||
const operation = parameters.operation ?? 'classify';
|
||||
|
||||
if (operation === 'classify') {
|
||||
return [{displayName: "Pass", type: "${NodeConnectionTypes.Main}"}, {displayName: "Fail", type: "${NodeConnectionTypes.Main}"}]
|
||||
}
|
||||
|
||||
return [{ displayName: "", type: "${NodeConnectionTypes.Main}"}]
|
||||
})($parameter)
|
||||
}}`,
|
||||
properties: [
|
||||
{
|
||||
displayName:
|
||||
'Use guardrails to validate text against a set of policies (e.g. NSFW, prompt injection) or to sanitize it (e.g. personal data, secret keys)',
|
||||
name: 'guardrailsUsage',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Check Text for Violations',
|
||||
value: 'classify',
|
||||
action: 'Check text for violations',
|
||||
description: 'Validate text against a set of policies (e.g. NSFW, prompt injection)',
|
||||
},
|
||||
{
|
||||
name: 'Sanitize Text',
|
||||
value: 'sanitize',
|
||||
action: 'Sanitize text',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-excess-final-period
|
||||
description: 'Redact text to mask personal data, secret keys, URLs, etc.',
|
||||
},
|
||||
],
|
||||
default: 'classify',
|
||||
},
|
||||
{
|
||||
displayName: 'Text To Check',
|
||||
name: 'text',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
typeOptions: {
|
||||
rows: 1,
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Check Text for Violations',
|
||||
value: 'classify',
|
||||
action: 'Check text for violations',
|
||||
description: 'Validate text against a set of policies (e.g. NSFW, prompt injection)',
|
||||
},
|
||||
{
|
||||
name: 'Sanitize Text',
|
||||
value: 'sanitize',
|
||||
action: 'Sanitize text',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-excess-final-period
|
||||
description: 'Redact text to mask personal data, secret keys, URLs, etc.',
|
||||
},
|
||||
],
|
||||
default: 'classify',
|
||||
},
|
||||
{
|
||||
displayName: 'Text To Check',
|
||||
name: 'text',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
typeOptions: {
|
||||
rows: 1,
|
||||
},
|
||||
{
|
||||
displayName: 'Guardrails',
|
||||
name: 'guardrails',
|
||||
placeholder: 'Add Guardrail',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Keywords',
|
||||
name: 'keywords',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'This guardrail checks if specified keywords appear in the input text and can be configured to trigger tripwires based on keyword matches. Multiple keywords can be added separated by comma.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/operation': ['classify'],
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Guardrails',
|
||||
name: 'guardrails',
|
||||
placeholder: 'Add Guardrail',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Keywords',
|
||||
name: 'keywords',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'This guardrail checks if specified keywords appear in the input text and can be configured to trigger tripwires based on keyword matches. Multiple keywords can be added separated by comma.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/operation': ['classify'],
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Jailbreak',
|
||||
name: 'jailbreak',
|
||||
type: 'fixedCollection',
|
||||
default: { value: { threshold: 0.7 } },
|
||||
description: 'Detects attempts to jailbreak or bypass AI safety measures',
|
||||
options: [wrapValue([THRESHOLD_OPTION, ...getPromptOption(JAILBREAK_PROMPT)])],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/operation': ['classify'],
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Jailbreak',
|
||||
name: 'jailbreak',
|
||||
type: 'fixedCollection',
|
||||
default: { value: { threshold: 0.7 } },
|
||||
description: 'Detects attempts to jailbreak or bypass AI safety measures',
|
||||
options: [wrapValue([THRESHOLD_OPTION, ...getPromptOption(JAILBREAK_PROMPT)])],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/operation': ['classify'],
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'NSFW',
|
||||
name: 'nsfw',
|
||||
type: 'fixedCollection',
|
||||
default: { value: { threshold: 0.7 } },
|
||||
description: 'Detects attempts to generate NSFW content',
|
||||
options: [wrapValue([THRESHOLD_OPTION, ...getPromptOption(NSFW_SYSTEM_PROMPT)])],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/operation': ['classify'],
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'NSFW',
|
||||
name: 'nsfw',
|
||||
type: 'fixedCollection',
|
||||
default: { value: { threshold: 0.7 } },
|
||||
description: 'Detects attempts to generate NSFW content',
|
||||
options: [wrapValue([THRESHOLD_OPTION, ...getPromptOption(NSFW_SYSTEM_PROMPT)])],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/operation': ['classify'],
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Personal Data (PII)',
|
||||
name: 'pii',
|
||||
type: 'fixedCollection',
|
||||
default: { value: { type: 'all' } },
|
||||
description: 'Detects attempts to use personal data content',
|
||||
options: [
|
||||
wrapValue([
|
||||
{
|
||||
displayName: 'Type',
|
||||
name: 'type',
|
||||
type: 'options',
|
||||
default: '',
|
||||
options: [
|
||||
{ name: 'All', value: 'all' },
|
||||
{ name: 'Selected', value: 'selected' },
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Entities',
|
||||
name: 'entities',
|
||||
type: 'multiOptions',
|
||||
default: [],
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['selected'],
|
||||
},
|
||||
},
|
||||
options: Object.values(PIIEntity).map((entity) => ({
|
||||
name: PII_NAME_MAP[entity],
|
||||
value: entity,
|
||||
})),
|
||||
},
|
||||
]),
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Secret Keys',
|
||||
name: 'secretKeys',
|
||||
type: 'fixedCollection',
|
||||
default: { value: { permissiveness: 'balanced' } },
|
||||
description:
|
||||
'Detects attempts to use secret keys in the input text. Scans text for common patterns, applies entropy analysis to detect random-looking strings.',
|
||||
options: [
|
||||
wrapValue([
|
||||
{
|
||||
displayName: 'Permissiveness',
|
||||
name: 'permissiveness',
|
||||
type: 'options',
|
||||
default: '',
|
||||
options: [
|
||||
{
|
||||
name: 'Strict',
|
||||
value: 'strict',
|
||||
description:
|
||||
'Most sensitive, may have more false positives (commonly flag high entropy filenames or code)',
|
||||
},
|
||||
{
|
||||
name: 'Balanced',
|
||||
value: 'balanced',
|
||||
description: 'Balanced between sensitivity and specificity',
|
||||
},
|
||||
{
|
||||
name: 'Permissive',
|
||||
value: 'permissive',
|
||||
description:
|
||||
'Least sensitive, may miss some secret keys (but also reduces false positives)',
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Topical Alignment',
|
||||
name: 'topicalAlignment',
|
||||
type: 'fixedCollection',
|
||||
default: { value: { threshold: 0.7 } },
|
||||
description: 'Detects attempts to stray from the business scope',
|
||||
options: [
|
||||
wrapValue([
|
||||
THRESHOLD_OPTION,
|
||||
...getPromptOption(
|
||||
TOPICAL_ALIGNMENT_SYSTEM_PROMPT,
|
||||
false,
|
||||
'Make sure you replace the placeholder.',
|
||||
),
|
||||
]),
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/operation': ['classify'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'URLs',
|
||||
name: 'urls',
|
||||
type: 'fixedCollection',
|
||||
default: { value: { allowedSchemes: ['https'], allowedUrls: '' } },
|
||||
description: 'Blocks URLs that are not in the allowed list',
|
||||
options: [
|
||||
wrapValue([
|
||||
{
|
||||
displayName: 'Block All URLs Except',
|
||||
name: 'allowedUrls',
|
||||
type: 'string',
|
||||
// keep placeholder to avoid limitation that removes collections with unchanged default values
|
||||
default: 'PLACEHOLDER',
|
||||
description:
|
||||
'Multiple URLs can be added separated by comma. Leave empty to block all URLs.',
|
||||
},
|
||||
{
|
||||
displayName: 'Allowed Schemes',
|
||||
name: 'allowedSchemes',
|
||||
type: 'multiOptions',
|
||||
default: ['https'],
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-multi-options-type-unsorted-items
|
||||
options: [
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
{ name: 'https', value: 'https' },
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
{ name: 'http', value: 'http' },
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
{ name: 'ftp', value: 'ftp' },
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
{ name: 'data', value: 'data' },
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
{ name: 'javascript', value: 'javascript' },
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
{ name: 'vbscript', value: 'vbscript' },
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
{ name: 'mailto', value: 'mailto' },
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Block Userinfo',
|
||||
name: 'blockUserinfo',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to block URLs with userinfo (user:pass@domain) to prevent credential injection',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/operation': ['classify'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Sanitize Userinfo',
|
||||
name: 'blockUserinfo',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to sanitize URLs with userinfo (user:pass@domain) to prevent credential injection',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/operation': ['sanitize'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Allow Subdomains',
|
||||
name: 'allowSubdomains',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to allow subdomains (e.g. sub.domain.com if domain.com is allowed)',
|
||||
},
|
||||
]),
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Custom',
|
||||
name: 'custom',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
sortable: true,
|
||||
multipleValues: true,
|
||||
},
|
||||
placeholder: 'Add Custom Guardrail',
|
||||
default: {
|
||||
guardrail: [{ name: 'Custom Guardrail' }],
|
||||
},
|
||||
options: [
|
||||
},
|
||||
{
|
||||
displayName: 'Personal Data (PII)',
|
||||
name: 'pii',
|
||||
type: 'fixedCollection',
|
||||
default: { value: { type: 'all' } },
|
||||
description: 'Detects attempts to use personal data content',
|
||||
options: [
|
||||
wrapValue([
|
||||
{
|
||||
displayName: 'Guardrail',
|
||||
name: 'guardrail',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Name of the custom guardrail',
|
||||
},
|
||||
THRESHOLD_OPTION,
|
||||
...getPromptOption('', false),
|
||||
displayName: 'Type',
|
||||
name: 'type',
|
||||
type: 'options',
|
||||
default: '',
|
||||
options: [
|
||||
{ name: 'All', value: 'all' },
|
||||
{ name: 'Selected', value: 'selected' },
|
||||
],
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/operation': ['classify'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Custom Regex',
|
||||
name: 'customRegex',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
sortable: true,
|
||||
multipleValues: true,
|
||||
},
|
||||
placeholder: 'Add Custom Regex',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Regex',
|
||||
name: 'regex',
|
||||
values: [
|
||||
displayName: 'Entities',
|
||||
name: 'entities',
|
||||
type: 'multiOptions',
|
||||
default: [],
|
||||
displayOptions: {
|
||||
show: {
|
||||
type: ['selected'],
|
||||
},
|
||||
},
|
||||
options: Object.values(PIIEntity).map((entity) => ({
|
||||
name: PII_NAME_MAP[entity],
|
||||
value: entity,
|
||||
})),
|
||||
},
|
||||
]),
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Secret Keys',
|
||||
name: 'secretKeys',
|
||||
type: 'fixedCollection',
|
||||
default: { value: { permissiveness: 'balanced' } },
|
||||
description:
|
||||
'Detects attempts to use secret keys in the input text. Scans text for common patterns, applies entropy analysis to detect random-looking strings.',
|
||||
options: [
|
||||
wrapValue([
|
||||
{
|
||||
displayName: 'Permissiveness',
|
||||
name: 'permissiveness',
|
||||
type: 'options',
|
||||
default: '',
|
||||
options: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
name: 'Strict',
|
||||
value: 'strict',
|
||||
description:
|
||||
'Name of the custom regex. Will be used for replacement when sanitizing.',
|
||||
'Most sensitive, may have more false positives (commonly flag high entropy filenames or code)',
|
||||
},
|
||||
{
|
||||
displayName: 'Regex',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Regex to match the input text',
|
||||
placeholder: '/text/gi',
|
||||
name: 'Balanced',
|
||||
value: 'balanced',
|
||||
description: 'Balanced between sensitivity and specificity',
|
||||
},
|
||||
{
|
||||
name: 'Permissive',
|
||||
value: 'permissive',
|
||||
description:
|
||||
'Least sensitive, may miss some secret keys (but also reduces false positives)',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Customize System Message',
|
||||
name: 'customizeSystemMessage',
|
||||
description:
|
||||
'Whether to customize the system message used by the guardrail to specify the output format',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/operation': ['classify'],
|
||||
]),
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Topical Alignment',
|
||||
name: 'topicalAlignment',
|
||||
type: 'fixedCollection',
|
||||
default: { value: { threshold: 0.7 } },
|
||||
description: 'Detects attempts to stray from the business scope',
|
||||
options: [
|
||||
wrapValue([
|
||||
THRESHOLD_OPTION,
|
||||
...getPromptOption(
|
||||
TOPICAL_ALIGNMENT_SYSTEM_PROMPT,
|
||||
false,
|
||||
'Make sure you replace the placeholder.',
|
||||
),
|
||||
]),
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/operation': ['classify'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'System Message',
|
||||
name: 'systemMessage',
|
||||
type: 'string',
|
||||
description:
|
||||
'The system message used by the guardrail to enforce thresholds and JSON output according to schema',
|
||||
hint: 'This message is appended after prompts defined by guardrails',
|
||||
default: LLM_SYSTEM_RULES,
|
||||
typeOptions: {
|
||||
rows: 6,
|
||||
{
|
||||
displayName: 'URLs',
|
||||
name: 'urls',
|
||||
type: 'fixedCollection',
|
||||
default: { value: { allowedSchemes: ['https'], allowedUrls: '' } },
|
||||
description: 'Blocks URLs that are not in the allowed list',
|
||||
options: [
|
||||
wrapValue([
|
||||
{
|
||||
displayName: 'Block All URLs Except',
|
||||
name: 'allowedUrls',
|
||||
type: 'string',
|
||||
// keep placeholder to avoid limitation that removes collections with unchanged default values
|
||||
default: 'PLACEHOLDER',
|
||||
description:
|
||||
'Multiple URLs can be added separated by comma. Leave empty to block all URLs.',
|
||||
},
|
||||
{
|
||||
displayName: 'Allowed Schemes',
|
||||
name: 'allowedSchemes',
|
||||
type: 'multiOptions',
|
||||
default: ['https'],
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-multi-options-type-unsorted-items
|
||||
options: [
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
{ name: 'https', value: 'https' },
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
{ name: 'http', value: 'http' },
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
{ name: 'ftp', value: 'ftp' },
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
{ name: 'data', value: 'data' },
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
{ name: 'javascript', value: 'javascript' },
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
{ name: 'vbscript', value: 'vbscript' },
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
{ name: 'mailto', value: 'mailto' },
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Block Userinfo',
|
||||
name: 'blockUserinfo',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to block URLs with userinfo (user:pass@domain) to prevent credential injection',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/operation': ['classify'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Sanitize Userinfo',
|
||||
name: 'blockUserinfo',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to sanitize URLs with userinfo (user:pass@domain) to prevent credential injection',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/operation': ['sanitize'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Allow Subdomains',
|
||||
name: 'allowSubdomains',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to allow subdomains (e.g. sub.domain.com if domain.com is allowed)',
|
||||
},
|
||||
]),
|
||||
],
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/customizeSystemMessage': [true],
|
||||
{
|
||||
displayName: 'Custom',
|
||||
name: 'custom',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
sortable: true,
|
||||
multipleValues: true,
|
||||
},
|
||||
placeholder: 'Add Custom Guardrail',
|
||||
default: {
|
||||
guardrail: [{ name: 'Custom Guardrail' }],
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Guardrail',
|
||||
name: 'guardrail',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Name of the custom guardrail',
|
||||
},
|
||||
THRESHOLD_OPTION,
|
||||
...getPromptOption('', false),
|
||||
],
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/operation': ['classify'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Custom Regex',
|
||||
name: 'customRegex',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
sortable: true,
|
||||
multipleValues: true,
|
||||
},
|
||||
placeholder: 'Add Custom Regex',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Regex',
|
||||
name: 'regex',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'Name of the custom regex. Will be used for replacement when sanitizing.',
|
||||
},
|
||||
{
|
||||
displayName: 'Regex',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Regex to match the input text',
|
||||
placeholder: '/text/gi',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Customize System Message',
|
||||
name: 'customizeSystemMessage',
|
||||
description:
|
||||
'Whether to customize the system message used by the guardrail to specify the output format',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/operation': ['classify'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
{
|
||||
displayName: 'System Message',
|
||||
name: 'systemMessage',
|
||||
type: 'string',
|
||||
description:
|
||||
'The system message used by the guardrail to enforce thresholds and JSON output according to schema',
|
||||
hint: 'This message is appended after prompts defined by guardrails',
|
||||
default: LLM_SYSTEM_RULES,
|
||||
typeOptions: {
|
||||
rows: 6,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/customizeSystemMessage': [true],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,4 +1,43 @@
|
||||
export const configureNodeInputs = (operation: 'classify' | 'sanitize') => {
|
||||
import type { GuardrailsOptions } from '../actions/types';
|
||||
|
||||
const LLM_CHECKS = ['nsfw', 'topicalAlignment', 'custom', 'jailbreak'] as const satisfies Array<
|
||||
keyof GuardrailsOptions
|
||||
>;
|
||||
|
||||
export const hasLLMGuardrails = (guardrails: GuardrailsOptions) => {
|
||||
const checks = Object.keys(guardrails ?? {});
|
||||
return checks.some((check) => (LLM_CHECKS as string[]).includes(check));
|
||||
};
|
||||
|
||||
export const configureNodeInputsV2 = (parameters: { guardrails: GuardrailsOptions }) => {
|
||||
// typeof LLM_CHECKS guarantees that it's in sync with hasLLMGuardrails
|
||||
const CHECKS: typeof LLM_CHECKS = ['nsfw', 'topicalAlignment', 'custom', 'jailbreak'];
|
||||
const checks = Object.keys(parameters?.guardrails ?? {});
|
||||
const hasLLMChecks = checks.some((check) => (CHECKS as string[]).includes(check));
|
||||
if (!hasLLMChecks) {
|
||||
return ['main'];
|
||||
}
|
||||
|
||||
return [
|
||||
'main',
|
||||
{
|
||||
type: 'ai_languageModel',
|
||||
displayName: 'Chat Model',
|
||||
maxConnections: 1,
|
||||
required: true,
|
||||
filter: {
|
||||
excludedNodes: [
|
||||
'@n8n/n8n-nodes-langchain.lmCohere',
|
||||
'@n8n/n8n-nodes-langchain.lmOllama',
|
||||
'n8n/n8n-nodes-langchain.lmOpenAi',
|
||||
'@n8n/n8n-nodes-langchain.lmOpenHuggingFaceInference',
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const configureNodeInputsV1 = (operation: 'classify' | 'sanitize') => {
|
||||
if (operation === 'sanitize') {
|
||||
// sanitize operations don't use a chat model
|
||||
return ['main'];
|
||||
|
||||
@@ -4,11 +4,10 @@ import type { IExecuteFunctions, INodeExecutionData, INode } from 'n8n-workflow'
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import * as ProcessActions from '../actions/process';
|
||||
import { Guardrails } from '../Guardrails.node';
|
||||
import * as ModelHelpers from '../helpers/model';
|
||||
import { execute } from '../actions/execute';
|
||||
|
||||
describe('Guardrails', () => {
|
||||
let guardrailsNode: Guardrails;
|
||||
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
|
||||
let mockNode: jest.Mocked<INode>;
|
||||
let mockModel: jest.Mocked<BaseChatModel>;
|
||||
@@ -16,13 +15,12 @@ describe('Guardrails', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
guardrailsNode = new Guardrails();
|
||||
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
mockNode = mock<INode>({
|
||||
id: 'test-node',
|
||||
name: 'Guardrails Node',
|
||||
type: 'n8n-nodes-langchain.guardrails',
|
||||
typeVersion: 1,
|
||||
typeVersion: 2,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
});
|
||||
@@ -41,6 +39,13 @@ describe('Guardrails', () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
operation: 'classify',
|
||||
guardrails: {
|
||||
nsfw: {
|
||||
value: {
|
||||
threshold: 0.5,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return params[paramName];
|
||||
});
|
||||
@@ -52,19 +57,18 @@ describe('Guardrails', () => {
|
||||
processSpy.mockResolvedValue({
|
||||
guardrailsInput: 'processed text',
|
||||
passed: {
|
||||
checks: [{ name: 'test', triggered: false }],
|
||||
checks: [{ name: 'nsfw', triggered: false }],
|
||||
},
|
||||
failed: null,
|
||||
});
|
||||
|
||||
const result = await guardrailsNode.execute.call(mockExecuteFunctions);
|
||||
const result = await execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toHaveLength(1);
|
||||
expect(result[0][0]).toEqual({
|
||||
json: {
|
||||
guardrailsInput: 'processed text',
|
||||
checks: [{ name: 'test', triggered: false }],
|
||||
checks: [{ name: 'nsfw', triggered: false }],
|
||||
},
|
||||
pairedItem: { item: 0 },
|
||||
});
|
||||
@@ -83,6 +87,13 @@ describe('Guardrails', () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
operation: 'classify',
|
||||
guardrails: {
|
||||
nsfw: {
|
||||
value: {
|
||||
threshold: 0.5,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return params[paramName];
|
||||
});
|
||||
@@ -114,7 +125,7 @@ describe('Guardrails', () => {
|
||||
failed: null,
|
||||
});
|
||||
|
||||
const result = await guardrailsNode.execute.call(mockExecuteFunctions);
|
||||
const result = await execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toHaveLength(3);
|
||||
@@ -167,7 +178,7 @@ describe('Guardrails', () => {
|
||||
failed: null,
|
||||
});
|
||||
|
||||
const result = await guardrailsNode.execute.call(mockExecuteFunctions);
|
||||
const result = await execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toHaveLength(2);
|
||||
@@ -218,9 +229,7 @@ describe('Guardrails', () => {
|
||||
const testError = new NodeOperationError(mockNode, 'Process failed');
|
||||
processSpy.mockRejectedValue(testError);
|
||||
|
||||
await expect(guardrailsNode.execute.bind(mockExecuteFunctions)()).rejects.toThrow(
|
||||
NodeOperationError,
|
||||
);
|
||||
await expect(execute.bind(mockExecuteFunctions)()).rejects.toThrow(NodeOperationError);
|
||||
});
|
||||
|
||||
it('should handle error gracefully when continueOnFail is true', async () => {
|
||||
@@ -242,7 +251,7 @@ describe('Guardrails', () => {
|
||||
const testError = new Error('Process failed');
|
||||
processSpy.mockRejectedValue(testError);
|
||||
|
||||
const result = await guardrailsNode.execute.call(mockExecuteFunctions);
|
||||
const result = await execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toHaveLength(0);
|
||||
@@ -290,7 +299,7 @@ describe('Guardrails', () => {
|
||||
failed: null,
|
||||
});
|
||||
|
||||
const result = await guardrailsNode.execute.call(mockExecuteFunctions);
|
||||
const result = await execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toHaveLength(2);
|
||||
@@ -342,7 +351,7 @@ describe('Guardrails', () => {
|
||||
failed: null,
|
||||
});
|
||||
|
||||
const result = await guardrailsNode.execute.call(mockExecuteFunctions);
|
||||
const result = await execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toHaveLength(1);
|
||||
@@ -371,7 +380,7 @@ describe('Guardrails', () => {
|
||||
failed: null,
|
||||
});
|
||||
|
||||
const result = await guardrailsNode.execute.call(mockExecuteFunctions);
|
||||
const result = await execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toHaveLength(1);
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import type { GuardrailsOptions } from '../../actions/types';
|
||||
import { configureNodeInputsV2, hasLLMGuardrails } from '../../helpers/configureNodeInputs';
|
||||
|
||||
describe('configureNodeInputs', () => {
|
||||
describe('hasLLMGuardrails+configureNodeInputs', () => {
|
||||
it.each([
|
||||
{
|
||||
guardrails: { nsfw: { value: { threshold: 0.5 } } },
|
||||
expected: true,
|
||||
expectedInputs: 2,
|
||||
name: 'nsfw',
|
||||
},
|
||||
{
|
||||
guardrails: { topicalAlignment: { value: { threshold: 0.7, prompt: 'test' } } },
|
||||
expected: true,
|
||||
expectedInputs: 2,
|
||||
name: 'topicalAlignment',
|
||||
},
|
||||
{
|
||||
guardrails: {
|
||||
custom: { guardrail: [{ name: 'custom', prompt: 'test prompt', threshold: 0.6 }] },
|
||||
},
|
||||
expected: true,
|
||||
expectedInputs: 2,
|
||||
name: 'custom',
|
||||
},
|
||||
{
|
||||
guardrails: { jailbreak: { value: { threshold: 0.8 } } },
|
||||
expected: true,
|
||||
expectedInputs: 2,
|
||||
name: 'jailbreak',
|
||||
},
|
||||
{
|
||||
guardrails: {
|
||||
nsfw: { value: { threshold: 0.5 } },
|
||||
topicalAlignment: { value: { threshold: 0.7, prompt: 'test' } },
|
||||
custom: { guardrail: [{ name: 'custom1', prompt: 'test prompt', threshold: 0.6 }] },
|
||||
jailbreak: { value: { threshold: 0.8 } },
|
||||
},
|
||||
expectedInputs: 2,
|
||||
name: 'multiple LLM checks',
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
guardrails: {
|
||||
keywords: 'test, keywords',
|
||||
pii: { value: { type: 'all' } },
|
||||
},
|
||||
expected: false,
|
||||
expectedInputs: 1,
|
||||
name: 'only non-LLM checks',
|
||||
},
|
||||
{
|
||||
guardrails: {},
|
||||
expected: false,
|
||||
expectedInputs: 1,
|
||||
name: 'empty guardrails',
|
||||
},
|
||||
{
|
||||
guardrails: undefined,
|
||||
expected: false,
|
||||
expectedInputs: 1,
|
||||
name: 'undefined guardrails',
|
||||
},
|
||||
])(
|
||||
'should return $expected when guardrails contain $name',
|
||||
({ guardrails, expected, expectedInputs }) => {
|
||||
expect(hasLLMGuardrails(guardrails as GuardrailsOptions)).toBe(expected);
|
||||
expect(configureNodeInputsV2({ guardrails: guardrails as GuardrailsOptions })).toHaveLength(
|
||||
expectedInputs,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -77,16 +77,16 @@ describe('Guardrails Process', () => {
|
||||
});
|
||||
}
|
||||
|
||||
it('Throws When Operation Is Classify And Model Is Null', async () => {
|
||||
it('Throws When Operation Is LLM-based And Model Is Null', async () => {
|
||||
setParams({
|
||||
text: 'hello',
|
||||
operation: 'classify',
|
||||
guardrails: {},
|
||||
guardrails: { nsfw: { value: { threshold: 0.5 } } },
|
||||
customizeSystemMessage: false,
|
||||
});
|
||||
|
||||
await expect(processGuardrails.call(exec, 0, null as unknown as BaseChatModel)).rejects.toThrow(
|
||||
'Chat Model is required for classify operation',
|
||||
'Chat Model is required',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
type INodeType,
|
||||
type INodeTypeBaseDescription,
|
||||
type INodeTypeDescription,
|
||||
type IExecuteFunctions,
|
||||
type INodeExecutionData,
|
||||
NodeConnectionTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { execute } from '../actions/execute';
|
||||
import { propertiesDescription } from '../description';
|
||||
import { configureNodeInputsV1 } from '../helpers/configureNodeInputs';
|
||||
|
||||
export class GuardrailsV1 implements INodeType {
|
||||
description: INodeTypeDescription;
|
||||
|
||||
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||
this.description = {
|
||||
...baseDescription,
|
||||
version: [1],
|
||||
inputs: `={{(${configureNodeInputsV1})($parameter.operation)}}`,
|
||||
outputs: `={{
|
||||
((parameters) => {
|
||||
const operation = parameters.operation ?? 'classify';
|
||||
|
||||
if (operation === 'classify') {
|
||||
return [{displayName: "Pass", type: "${NodeConnectionTypes.Main}"}, {displayName: "Fail", type: "${NodeConnectionTypes.Main}"}]
|
||||
}
|
||||
|
||||
return [{ displayName: "", type: "${NodeConnectionTypes.Main}"}]
|
||||
})($parameter)
|
||||
}}`,
|
||||
defaults: {
|
||||
name: 'Guardrails',
|
||||
},
|
||||
properties: propertiesDescription,
|
||||
};
|
||||
}
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
return await execute.call(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
type INodeType,
|
||||
type INodeTypeBaseDescription,
|
||||
type INodeTypeDescription,
|
||||
type IExecuteFunctions,
|
||||
type INodeExecutionData,
|
||||
NodeConnectionTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { execute } from '../actions/execute';
|
||||
import { propertiesDescription } from '../description';
|
||||
import { configureNodeInputsV2 } from '../helpers/configureNodeInputs';
|
||||
|
||||
export class GuardrailsV2 implements INodeType {
|
||||
description: INodeTypeDescription;
|
||||
|
||||
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||
this.description = {
|
||||
...baseDescription,
|
||||
version: [2],
|
||||
inputs: `={{(${configureNodeInputsV2})($parameter)}}`,
|
||||
outputs: `={{
|
||||
((parameters) => {
|
||||
const operation = parameters.operation ?? 'classify';
|
||||
|
||||
if (operation === 'classify') {
|
||||
return [{displayName: "Pass", type: "${NodeConnectionTypes.Main}"}, {displayName: "Fail", type: "${NodeConnectionTypes.Main}"}]
|
||||
}
|
||||
|
||||
return [{ displayName: "", type: "${NodeConnectionTypes.Main}"}]
|
||||
})($parameter)
|
||||
}}`,
|
||||
defaults: {
|
||||
name: 'Guardrails',
|
||||
},
|
||||
properties: propertiesDescription,
|
||||
};
|
||||
}
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
return await execute.call(this);
|
||||
}
|
||||
}
|
||||
@@ -269,8 +269,13 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
|
||||
});
|
||||
|
||||
const isConfigurableNode = computed(() => {
|
||||
return (workflow: Workflow, node: INode, nodeTypeName: string): boolean => {
|
||||
const nodeType = getNodeType.value(nodeTypeName);
|
||||
return (
|
||||
workflow: Workflow,
|
||||
node: INode,
|
||||
nodeTypeName: string,
|
||||
nodeTypeVersion?: number,
|
||||
): boolean => {
|
||||
const nodeType = getNodeType.value(nodeTypeName, nodeTypeVersion);
|
||||
if (nodeType === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
+6
-1
@@ -116,7 +116,12 @@ export function useCanvasMapping({
|
||||
options: {
|
||||
trigger: isTriggerNodeById.value[node.id],
|
||||
configuration: nodeTypesStore.isConfigNode(workflowObject.value, node, node.type),
|
||||
configurable: nodeTypesStore.isConfigurableNode(workflowObject.value, node, node.type),
|
||||
configurable: nodeTypesStore.isConfigurableNode(
|
||||
workflowObject.value,
|
||||
node,
|
||||
node.type,
|
||||
node.typeVersion,
|
||||
),
|
||||
inputs: {
|
||||
labelSize: nodeInputLabelSizeById.value[node.id],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user