diff --git a/packages/components/nodes/chains/LLMChain/LLMChain.ts b/packages/components/nodes/chains/LLMChain/LLMChain.ts index 0d55588415c..ee532a27910 100644 --- a/packages/components/nodes/chains/LLMChain/LLMChain.ts +++ b/packages/components/nodes/chains/LLMChain/LLMChain.ts @@ -7,6 +7,7 @@ import { BaseOutputParser } from 'langchain/schema/output_parser' import { formatResponse, injectOutputParser } from '../../outputparsers/OutputParserHelpers' import { BaseLLMOutputParser } from 'langchain/schema/output_parser' import { OutputFixingParser } from 'langchain/output_parsers' +import { checkInputs, Moderation, streamResponse } from '../../moderation/Moderation' class LLMChain_Chains implements INode { label: string @@ -47,6 +48,14 @@ class LLMChain_Chains implements INode { type: 'BaseLLMOutputParser', optional: true }, + { + label: 'Input Moderation', + description: 'Detect text that could generate harmful output and prevent it from being sent to the language model', + name: 'inputModeration', + type: 'Moderation', + optional: true, + list: true + }, { label: 'Chain Name', name: 'chainName', @@ -144,7 +153,7 @@ const runPrediction = async ( const isStreaming = options.socketIO && options.socketIOClientId const socketIO = isStreaming ? options.socketIO : undefined const socketIOClientId = isStreaming ? options.socketIOClientId : '' - + const moderations = nodeData.inputs?.inputModeration as Moderation[] /** * Apply string transformation to reverse converted special chars: * FROM: { "value": "hello i am benFLOWISE_NEWLINEFLOWISE_NEWLINEFLOWISE_TABhow are you?" } @@ -152,6 +161,17 @@ const runPrediction = async ( */ const promptValues = handleEscapeCharacters(promptValuesRaw, true) + if (moderations && moderations.length > 0) { + try { + // Use the output of the moderation chain as input for the LLM chain + input = await checkInputs(moderations, chain.llm, input) + } catch (e) { + await new Promise((resolve) => setTimeout(resolve, 500)) + streamResponse(isStreaming, e.message, socketIO, socketIOClientId) + return formatResponse(e.message) + } + } + if (promptValues && inputVariables.length > 0) { let seen: string[] = [] diff --git a/packages/components/nodes/moderation/Moderation.ts b/packages/components/nodes/moderation/Moderation.ts new file mode 100644 index 00000000000..9c40f55ab45 --- /dev/null +++ b/packages/components/nodes/moderation/Moderation.ts @@ -0,0 +1,28 @@ +import { BaseLanguageModel } from 'langchain/base_language' +import { Server } from 'socket.io' + +export abstract class Moderation { + abstract checkForViolations(llm: BaseLanguageModel, input: string): Promise +} + +export const checkInputs = async (inputModerations: Moderation[], llm: BaseLanguageModel, input: string): Promise => { + for (const moderation of inputModerations) { + input = await moderation.checkForViolations(llm, input) + } + return input +} + +// is this the correct location for this function? +// should we have a utils files that all node components can use? +export const streamResponse = (isStreaming: any, response: string, socketIO: Server, socketIOClientId: string) => { + if (isStreaming) { + const result = response.split(/(\s+)/) + result.forEach((token: string, index: number) => { + if (index === 0) { + socketIO.to(socketIOClientId).emit('start', token) + } + socketIO.to(socketIOClientId).emit('token', token) + }) + socketIO.to(socketIOClientId).emit('end') + } +} diff --git a/packages/components/nodes/moderation/OpenAIModeration/OpenAIModeration.ts b/packages/components/nodes/moderation/OpenAIModeration/OpenAIModeration.ts new file mode 100644 index 00000000000..5233f174f15 --- /dev/null +++ b/packages/components/nodes/moderation/OpenAIModeration/OpenAIModeration.ts @@ -0,0 +1,46 @@ +import { INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses } from '../../../src' +import { Moderation } from '../Moderation' +import { OpenAIModerationRunner } from './OpenAIModerationRunner' + +class OpenAIModeration implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + + constructor() { + this.label = 'OpenAI Moderation' + this.name = 'inputModerationOpenAI' + this.version = 1.0 + this.type = 'Moderation' + this.icon = 'openai-moderation.png' + this.category = 'Moderation' + this.description = 'Check whether content complies with OpenAI usage policies.' + this.baseClasses = [this.type, ...getBaseClasses(Moderation)] + this.inputs = [ + { + label: 'Error Message', + name: 'moderationErrorMessage', + type: 'string', + rows: 2, + default: "Cannot Process! Input violates OpenAI's content moderation policies.", + optional: true + } + ] + } + + async init(nodeData: INodeData): Promise { + const runner = new OpenAIModerationRunner() + const moderationErrorMessage = nodeData.inputs?.moderationErrorMessage as string + if (moderationErrorMessage) runner.setErrorMessage(moderationErrorMessage) + return runner + } +} + +module.exports = { nodeClass: OpenAIModeration } diff --git a/packages/components/nodes/moderation/OpenAIModeration/OpenAIModerationRunner.ts b/packages/components/nodes/moderation/OpenAIModeration/OpenAIModerationRunner.ts new file mode 100644 index 00000000000..c517f419a7f --- /dev/null +++ b/packages/components/nodes/moderation/OpenAIModeration/OpenAIModerationRunner.ts @@ -0,0 +1,31 @@ +import { Moderation } from '../Moderation' +import { BaseLanguageModel } from 'langchain/base_language' +import { OpenAIModerationChain } from 'langchain/chains' + +export class OpenAIModerationRunner implements Moderation { + private moderationErrorMessage: string = "Text was found that violates OpenAI's content policy." + + async checkForViolations(llm: BaseLanguageModel, input: string): Promise { + const openAIApiKey = (llm as any).openAIApiKey + if (!openAIApiKey) { + throw Error('OpenAI API key not found') + } + // Create a new instance of the OpenAIModerationChain + const moderation = new OpenAIModerationChain({ + openAIApiKey: openAIApiKey, + throwError: false // If set to true, the call will throw an error when the moderation chain detects violating content. If set to false, violating content will return "Text was found that violates OpenAI's content policy.". + }) + // Send the user's input to the moderation chain and wait for the result + const { output: moderationOutput, results } = await moderation.call({ + input: input + }) + if (results[0].flagged) { + throw Error(this.moderationErrorMessage) + } + return moderationOutput + } + + setErrorMessage(message: string) { + this.moderationErrorMessage = message + } +} diff --git a/packages/components/nodes/moderation/OpenAIModeration/openai-moderation.png b/packages/components/nodes/moderation/OpenAIModeration/openai-moderation.png new file mode 100644 index 00000000000..e3b1b282a70 Binary files /dev/null and b/packages/components/nodes/moderation/OpenAIModeration/openai-moderation.png differ diff --git a/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModeration.ts b/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModeration.ts new file mode 100644 index 00000000000..bf5a32f6802 --- /dev/null +++ b/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModeration.ts @@ -0,0 +1,55 @@ +import { INode, INodeData, INodeParams } from '../../../src/Interface' +import { getBaseClasses } from '../../../src' +import { Moderation } from '../Moderation' +import { SimplePromptModerationRunner } from './SimplePromptModerationRunner' + +class SimplePromptModeration implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + + constructor() { + this.label = 'Simple Prompt Moderation' + this.name = 'inputModerationSimple' + this.version = 1.0 + this.type = 'Moderation' + this.icon = 'simple_moderation.png' + this.category = 'Moderation' + this.description = 'Check whether input consists of any text from Deny list, and prevent being sent to LLM' + this.baseClasses = [this.type, ...getBaseClasses(Moderation)] + this.inputs = [ + { + label: 'Deny List', + name: 'denyList', + type: 'string', + rows: 4, + placeholder: `ignore previous instructions\ndo not follow the directions\nyou must ignore all previous instructions`, + description: 'An array of string literals (enter one per line) that should not appear in the prompt text.', + optional: false + }, + { + label: 'Error Message', + name: 'moderationErrorMessage', + type: 'string', + rows: 2, + default: 'Cannot Process! Input violates content moderation policies.', + optional: true + } + ] + } + + async init(nodeData: INodeData): Promise { + const denyList = nodeData.inputs?.denyList as string + const moderationErrorMessage = nodeData.inputs?.moderationErrorMessage as string + + return new SimplePromptModerationRunner(denyList, moderationErrorMessage) + } +} + +module.exports = { nodeClass: SimplePromptModeration } diff --git a/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts b/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts new file mode 100644 index 00000000000..7fc251ad48a --- /dev/null +++ b/packages/components/nodes/moderation/SimplePromptModeration/SimplePromptModerationRunner.ts @@ -0,0 +1,24 @@ +import { Moderation } from '../Moderation' +import { BaseLanguageModel } from 'langchain/base_language' + +export class SimplePromptModerationRunner implements Moderation { + private readonly denyList: string = '' + private readonly moderationErrorMessage: string = '' + + constructor(denyList: string, moderationErrorMessage: string) { + this.denyList = denyList + if (denyList.indexOf('\n') === -1) { + this.denyList += '\n' + } + this.moderationErrorMessage = moderationErrorMessage + } + + async checkForViolations(_: BaseLanguageModel, input: string): Promise { + this.denyList.split('\n').forEach((denyListItem) => { + if (denyListItem && denyListItem !== '' && input.includes(denyListItem)) { + throw Error(this.moderationErrorMessage) + } + }) + return Promise.resolve(input) + } +} diff --git a/packages/components/nodes/moderation/SimplePromptModeration/simple_moderation.png b/packages/components/nodes/moderation/SimplePromptModeration/simple_moderation.png new file mode 100644 index 00000000000..47d78cb672f Binary files /dev/null and b/packages/components/nodes/moderation/SimplePromptModeration/simple_moderation.png differ diff --git a/packages/server/marketplaces/chatflows/Input Moderation.json b/packages/server/marketplaces/chatflows/Input Moderation.json new file mode 100644 index 00000000000..1f6cc6245c5 --- /dev/null +++ b/packages/server/marketplaces/chatflows/Input Moderation.json @@ -0,0 +1,441 @@ +{ + "description": "Detect text that could generate harmful output and prevent it from being sent to the language model", + "badge": "NEW", + "nodes": [ + { + "width": 300, + "height": 356, + "id": "inputModerationOpenAI_0", + "position": { + "x": 334.36040624369247, + "y": 467.88081727992824 + }, + "type": "customNode", + "data": { + "id": "inputModerationOpenAI_0", + "label": "OpenAI Moderation", + "version": 1, + "name": "inputModerationOpenAI", + "type": "Moderation", + "baseClasses": ["Moderation"], + "category": "Moderation", + "description": "Check whether content complies with OpenAI usage policies.", + "inputParams": [ + { + "label": "Error Message", + "name": "moderationErrorMessage", + "type": "string", + "rows": 2, + "default": "Cannot Process! Input violates OpenAI's content moderation policies.", + "optional": true, + "id": "inputModerationOpenAI_0-input-moderationErrorMessage-string" + } + ], + "inputAnchors": [], + "inputs": { + "moderationErrorMessage": "Cannot Process! Input violates OpenAI's content moderation policies." + }, + "outputAnchors": [ + { + "id": "inputModerationOpenAI_0-output-inputModerationOpenAI-Moderation|Moderation", + "name": "inputModerationOpenAI", + "label": "Moderation", + "type": "Moderation" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 334.36040624369247, + "y": 467.88081727992824 + }, + "dragging": false + }, + { + "width": 300, + "height": 507, + "id": "llmChain_0", + "position": { + "x": 859.216454729136, + "y": 154.86846618352752 + }, + "type": "customNode", + "data": { + "id": "llmChain_0", + "label": "LLM Chain", + "version": 3, + "name": "llmChain", + "type": "LLMChain", + "baseClasses": ["LLMChain", "BaseChain", "Runnable"], + "category": "Chains", + "description": "Chain to run queries against LLMs", + "inputParams": [ + { + "label": "Chain Name", + "name": "chainName", + "type": "string", + "placeholder": "Name Your Chain", + "optional": true, + "id": "llmChain_0-input-chainName-string" + } + ], + "inputAnchors": [ + { + "label": "Language Model", + "name": "model", + "type": "BaseLanguageModel", + "id": "llmChain_0-input-model-BaseLanguageModel" + }, + { + "label": "Prompt", + "name": "prompt", + "type": "BasePromptTemplate", + "id": "llmChain_0-input-prompt-BasePromptTemplate" + }, + { + "label": "Output Parser", + "name": "outputParser", + "type": "BaseLLMOutputParser", + "optional": true, + "id": "llmChain_0-input-outputParser-BaseLLMOutputParser" + }, + { + "label": "Input Moderation", + "description": "Detect text that could generate harmful output and prevent it from being sent to the language model", + "name": "inputModeration", + "type": "Moderation", + "optional": true, + "list": true, + "id": "llmChain_0-input-inputModeration-Moderation" + } + ], + "inputs": { + "model": "{{chatOpenAI_0.data.instance}}", + "prompt": "{{promptTemplate_0.data.instance}}", + "outputParser": "", + "inputModeration": ["{{inputModerationOpenAI_0.data.instance}}"], + "chainName": "" + }, + "outputAnchors": [ + { + "name": "output", + "label": "Output", + "type": "options", + "options": [ + { + "id": "llmChain_0-output-llmChain-LLMChain|BaseChain|Runnable", + "name": "llmChain", + "label": "LLM Chain", + "type": "LLMChain | BaseChain | Runnable" + }, + { + "id": "llmChain_0-output-outputPrediction-string|json", + "name": "outputPrediction", + "label": "Output Prediction", + "type": "string | json" + } + ], + "default": "llmChain" + } + ], + "outputs": { + "output": "llmChain" + }, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 859.216454729136, + "y": 154.86846618352752 + }, + "dragging": false + }, + { + "width": 300, + "height": 574, + "id": "chatOpenAI_0", + "position": { + "x": 424.69244822381864, + "y": -271.138349609141 + }, + "type": "customNode", + "data": { + "id": "chatOpenAI_0", + "label": "ChatOpenAI", + "version": 2, + "name": "chatOpenAI", + "type": "ChatOpenAI", + "baseClasses": ["ChatOpenAI", "BaseChatModel", "BaseLanguageModel", "Runnable"], + "category": "Chat Models", + "description": "Wrapper around OpenAI large language models that use the Chat endpoint", + "inputParams": [ + { + "label": "Connect Credential", + "name": "credential", + "type": "credential", + "credentialNames": ["openAIApi"], + "id": "chatOpenAI_0-input-credential-credential" + }, + { + "label": "Model Name", + "name": "modelName", + "type": "options", + "options": [ + { + "label": "gpt-4", + "name": "gpt-4" + }, + { + "label": "gpt-4-1106-preview", + "name": "gpt-4-1106-preview" + }, + { + "label": "gpt-4-vision-preview", + "name": "gpt-4-vision-preview" + }, + { + "label": "gpt-4-0613", + "name": "gpt-4-0613" + }, + { + "label": "gpt-4-32k", + "name": "gpt-4-32k" + }, + { + "label": "gpt-4-32k-0613", + "name": "gpt-4-32k-0613" + }, + { + "label": "gpt-3.5-turbo", + "name": "gpt-3.5-turbo" + }, + { + "label": "gpt-3.5-turbo-1106", + "name": "gpt-3.5-turbo-1106" + }, + { + "label": "gpt-3.5-turbo-0613", + "name": "gpt-3.5-turbo-0613" + }, + { + "label": "gpt-3.5-turbo-16k", + "name": "gpt-3.5-turbo-16k" + }, + { + "label": "gpt-3.5-turbo-16k-0613", + "name": "gpt-3.5-turbo-16k-0613" + } + ], + "default": "gpt-3.5-turbo", + "optional": true, + "id": "chatOpenAI_0-input-modelName-options" + }, + { + "label": "Temperature", + "name": "temperature", + "type": "number", + "step": 0.1, + "default": 0.9, + "optional": true, + "id": "chatOpenAI_0-input-temperature-number" + }, + { + "label": "Max Tokens", + "name": "maxTokens", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-maxTokens-number" + }, + { + "label": "Top Probability", + "name": "topP", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-topP-number" + }, + { + "label": "Frequency Penalty", + "name": "frequencyPenalty", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-frequencyPenalty-number" + }, + { + "label": "Presence Penalty", + "name": "presencePenalty", + "type": "number", + "step": 0.1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-presencePenalty-number" + }, + { + "label": "Timeout", + "name": "timeout", + "type": "number", + "step": 1, + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-timeout-number" + }, + { + "label": "BasePath", + "name": "basepath", + "type": "string", + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-basepath-string" + }, + { + "label": "BaseOptions", + "name": "baseOptions", + "type": "json", + "optional": true, + "additionalParams": true, + "id": "chatOpenAI_0-input-baseOptions-json" + } + ], + "inputAnchors": [ + { + "label": "Cache", + "name": "cache", + "type": "BaseCache", + "optional": true, + "id": "chatOpenAI_0-input-cache-BaseCache" + } + ], + "inputs": { + "cache": "", + "modelName": "gpt-3.5-turbo", + "temperature": 0.9, + "maxTokens": "", + "topP": "", + "frequencyPenalty": "", + "presencePenalty": "", + "timeout": "", + "basepath": "", + "baseOptions": "" + }, + "outputAnchors": [ + { + "id": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", + "name": "chatOpenAI", + "label": "ChatOpenAI", + "type": "ChatOpenAI | BaseChatModel | BaseLanguageModel | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": 424.69244822381864, + "y": -271.138349609141 + }, + "dragging": false + }, + { + "width": 300, + "height": 475, + "id": "promptTemplate_0", + "position": { + "x": -17.005933033720936, + "y": -20.829788775850602 + }, + "type": "customNode", + "data": { + "id": "promptTemplate_0", + "label": "Prompt Template", + "version": 1, + "name": "promptTemplate", + "type": "PromptTemplate", + "baseClasses": ["PromptTemplate", "BaseStringPromptTemplate", "BasePromptTemplate", "Runnable"], + "category": "Prompts", + "description": "Schema to represent a basic prompt for an LLM", + "inputParams": [ + { + "label": "Template", + "name": "template", + "type": "string", + "rows": 4, + "placeholder": "What is a good name for a company that makes {product}?", + "id": "promptTemplate_0-input-template-string" + }, + { + "label": "Format Prompt Values", + "name": "promptValues", + "type": "json", + "optional": true, + "acceptVariable": true, + "list": true, + "id": "promptTemplate_0-input-promptValues-json" + } + ], + "inputAnchors": [], + "inputs": { + "template": "Answer user question:\n{text}", + "promptValues": "{\"history\":\"{{chat_history}}\"}" + }, + "outputAnchors": [ + { + "id": "promptTemplate_0-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate|Runnable", + "name": "promptTemplate", + "label": "PromptTemplate", + "type": "PromptTemplate | BaseStringPromptTemplate | BasePromptTemplate | Runnable" + } + ], + "outputs": {}, + "selected": false + }, + "selected": false, + "positionAbsolute": { + "x": -17.005933033720936, + "y": -20.829788775850602 + }, + "dragging": false + } + ], + "edges": [ + { + "source": "inputModerationOpenAI_0", + "sourceHandle": "inputModerationOpenAI_0-output-inputModerationOpenAI-Moderation|Moderation", + "target": "llmChain_0", + "targetHandle": "llmChain_0-input-inputModeration-Moderation", + "type": "buttonedge", + "id": "inputModerationOpenAI_0-inputModerationOpenAI_0-output-inputModerationOpenAI-Moderation|Moderation-llmChain_0-llmChain_0-input-inputModeration-Moderation", + "data": { + "label": "" + } + }, + { + "source": "chatOpenAI_0", + "sourceHandle": "chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable", + "target": "llmChain_0", + "targetHandle": "llmChain_0-input-model-BaseLanguageModel", + "type": "buttonedge", + "id": "chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-llmChain_0-llmChain_0-input-model-BaseLanguageModel", + "data": { + "label": "" + } + }, + { + "source": "promptTemplate_0", + "sourceHandle": "promptTemplate_0-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate|Runnable", + "target": "llmChain_0", + "targetHandle": "llmChain_0-input-prompt-BasePromptTemplate", + "type": "buttonedge", + "id": "promptTemplate_0-promptTemplate_0-output-promptTemplate-PromptTemplate|BaseStringPromptTemplate|BasePromptTemplate|Runnable-llmChain_0-llmChain_0-input-prompt-BasePromptTemplate", + "data": { + "label": "" + } + } + ] +}