diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index fbf4ea1299e..40589ff50e0 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -354,3 +354,15 @@ export interface IVariableOverride { // DocumentStore related export * from './Interface.DocumentStore' + +export enum RedactionRuleType { + REMOVE = 'remove', + REPLACE = 'replace', + ASTERISK = 'asterisk' +} +export interface RedactionRule { + phrase: string + isRegexp: boolean + type: RedactionRuleType + replacement: string +} diff --git a/packages/server/src/utils/buildChatflow.ts b/packages/server/src/utils/buildChatflow.ts index f73f6044398..86d5dbd73c6 100644 --- a/packages/server/src/utils/buildChatflow.ts +++ b/packages/server/src/utils/buildChatflow.ts @@ -32,7 +32,9 @@ import { IVariable, INodeOverrides, IVariableOverride, - MODE + MODE, + RedactionRule, + RedactionRuleType } from '../Interface' import { InternalFlowiseError } from '../errors/internalFlowiseError' import { databaseEntities } from '.' @@ -653,26 +655,62 @@ export const executeFlow = async ({ resultText = result.text /* Check for post-processing settings */ if (chatflowConfig?.postProcessing?.enabled === true) { + let moderatedResponse = resultText try { - const postProcessingFunction = JSON.parse(chatflowConfig?.postProcessing?.customFunction) - const nodeInstanceFilePath = componentNodes['customFunction'].filePath as string - const nodeModule = await import(nodeInstanceFilePath) - const nodeData = { - inputs: { javascriptFunction: postProcessingFunction }, - outputs: { output: 'output' } + //first run redaction rules + if (chatflowConfig.postProcessing.redactionRules) { + const redactionRules: RedactionRule[] = JSON.parse(chatflowConfig.postProcessing.redactionRules) + for (const rule of redactionRules) { + switch (rule.type) { + case RedactionRuleType.REMOVE: + if (rule.isRegexp) { + const regex = new RegExp(rule.phrase, 'g') + moderatedResponse = moderatedResponse.replace(regex, '') + } else { + moderatedResponse = moderatedResponse.replace(rule.phrase, '') + } + break + case RedactionRuleType.REPLACE: + if (rule.isRegexp) { + const regex = new RegExp(rule.phrase, 'g') + moderatedResponse = moderatedResponse.replace(regex, rule.replacement) + } else { + moderatedResponse = moderatedResponse.replace(rule.phrase, rule.replacement) + } + break + case RedactionRuleType.ASTERISK: + if (rule.isRegexp) { + const regex = new RegExp(rule.phrase, 'g') + moderatedResponse = moderatedResponse.replace(regex, '***') + } else { + moderatedResponse = moderatedResponse.replace(rule.phrase, '***') + } + break + } + } } - const options: ICommonObject = { - chatflowid: chatflow.id, - sessionId, - chatId, - input: question, - rawOutput: resultText, - appDataSource, - databaseEntities, - logger + //second - apply custom function + if (chatflowConfig?.postProcessing?.customFunction) { + const postProcessingFunction = JSON.parse(chatflowConfig?.postProcessing?.customFunction) + const nodeInstanceFilePath = componentNodes['customFunction'].filePath as string + const nodeModule = await import(nodeInstanceFilePath) + const nodeData = { + inputs: { javascriptFunction: postProcessingFunction }, + outputs: { output: 'output' } + } + const options: ICommonObject = { + chatflowid: chatflow.id, + sessionId, + chatId, + input: question, + rawOutput: moderatedResponse, + appDataSource, + databaseEntities, + logger + } + const customFuncNodeInstance = new nodeModule.nodeClass() + moderatedResponse = await customFuncNodeInstance.init(nodeData, question, options) } - const customFuncNodeInstance = new nodeModule.nodeClass() - let moderatedResponse = await customFuncNodeInstance.init(nodeData, question, options) result.text = moderatedResponse resultText = result.text } catch (e) { diff --git a/packages/ui/src/ui-component/extended/PostProcessing.jsx b/packages/ui/src/ui-component/extended/PostProcessing.jsx index fd56a3eb683..62b144c9c3e 100644 --- a/packages/ui/src/ui-component/extended/PostProcessing.jsx +++ b/packages/ui/src/ui-component/extended/PostProcessing.jsx @@ -4,13 +4,14 @@ import PropTypes from 'prop-types' import { useSelector } from 'react-redux' // material-ui -import { IconButton, Button, Box, Typography } from '@mui/material' -import { IconArrowsMaximize, IconBulb, IconX } from '@tabler/icons-react' +import { IconButton, Button, Box, Typography, Tabs, Tab, Stack, OutlinedInput, Divider, Chip, Breadcrumbs } from '@mui/material' +import { IconArrowRight, IconArrowsMaximize, IconBulb, IconX } from '@tabler/icons-react' import { useTheme } from '@mui/material/styles' // Project import import { StyledButton } from '@/ui-component/button/StyledButton' import { SwitchInput } from '@/ui-component/switch/Switch' +import { TabPanel } from '@/ui-component/tabs/TabPanel' import { CodeEditor } from '@/ui-component/editor/CodeEditor' import ExpandTextDialog from '@/ui-component/dialog/ExpandTextDialog' @@ -20,8 +21,48 @@ import useNotifier from '@/utils/useNotifier' // API import chatflowsApi from '@/api/chatflows' +import { Dropdown } from '@/ui-component/dropdown/Dropdown' const sampleFunction = `return $flow.rawOutput + " This is a post processed response!";` +const redactionTypes = [ + { + label: 'Remove matching text', + name: 'remove' + }, + { + label: 'Replace with custom text', + name: 'replace' + }, + { + label: 'Replace with ***', + name: 'asterisk' + } +] + +function HorizontalStepper() { + const breadcrumbs = [ + + Generate LLM output + , + + Apply Redaction Rules + , + + Execute Custom JS Function + , + + Show Response + + ] + return ( + + When enabled, the order of Execution + } aria-label='breadcrumb'> + {breadcrumbs} + + + ) +} const PostProcessing = ({ dialogProps }) => { const dispatch = useDispatch() @@ -39,6 +80,38 @@ const PostProcessing = ({ dialogProps }) => { const [showExpandDialog, setShowExpandDialog] = useState(false) const [expandDialogProps, setExpandDialogProps] = useState({}) + const [activeTabValue, setActiveTabValue] = useState(0) + + const [replacementText, setReplacementText] = useState('') + const [redactionPhrase, setRedactionPhrase] = useState('') + const [isRegexp, setRegexp] = useState(false) + const [redactionType, setRedactionType] = useState('asterisk') + const [redactionRules, setRedactionRules] = useState([]) + + const addRedactionRule = () => { + const newRule = { + phrase: redactionPhrase, + isRegexp: isRegexp, + type: redactionType, + replacement: redactionType === 'replace' ? replacementText : '' + } + setRedactionRules([...redactionRules, newRule]) + setRedactionPhrase('') + setRedactionType('asterisk') + setReplacementText('') + setRegexp(false) + } + + const removeRedactionRule = (index) => { + const rows = [...redactionRules] + rows.splice(index, 1) + setRedactionRules(rows) + } + + const handleTabChange = (event, newValue) => { + setActiveTabValue(newValue) + } + const handleChange = (value) => { setPostProcessingEnabled(value) } @@ -66,6 +139,7 @@ const PostProcessing = ({ dialogProps }) => { let value = { postProcessing: { enabled: postProcessingEnabled, + redactionRules: JSON.stringify(redactionRules), customFunction: JSON.stringify(postProcessingFunction) } } @@ -116,10 +190,19 @@ const PostProcessing = ({ dialogProps }) => { if (chatbotConfig.postProcessing.customFunction) { setPostProcessingFunction(JSON.parse(chatbotConfig.postProcessing.customFunction)) } + if (chatbotConfig.postProcessing.redactionRules) { + setRedactionRules(JSON.parse(chatbotConfig.postProcessing.redactionRules)) + } } } - return () => {} + return () => { + setRedactionRules([]) + setRedactionPhrase('') + setRedactionType('asterisk') + setReplacementText('') + setRegexp(false) + } }, [dialogProps]) return ( @@ -127,79 +210,244 @@ const PostProcessing = ({ dialogProps }) => { - - - JS Function - -
- onExpandDialogClicked(postProcessingFunction)} - > - - - - -
- setPostProcessingFunction(code)} - basicSetup={{ highlightActiveLine: false, highlightActiveLineGutter: false }} - /> -
-
+ + + +
+ + + + + + } spacing={2}> + + + + Phrase or Pattern +
+ + setRedactionPhrase(e.target.value)} + size='small' + value={redactionPhrase} + name='redactionPhrase' + placeholder='Enter a phrase or regexp to redact' + /> + +
+
+ + Is RegExp? +
+ + + +
+
+
+ + + Action + setRedactionType(newValue)} + value={redactionType ?? 'choose an option'} + /> + + {redactionType === 'replace' && ( + + Replacement Text + setReplacementText(e.target.value)} + size='small' + value={replacementText} + name='replacementText' + placeholder='Enter replacement text' + /> + + )} + + + + Add Redaction rule + + +
+ + + Active Redaction Rules + + + {redactionRules.map((rule, index) => ( + + + {rule.isRegexp ? ( + + ) : ( + + )} + {rule.type === 'remove' && ( + {`Remove: ${rule.phrase}`} + )} + {rule.type === 'replace' && ( + {`Replace: ${rule.phrase} with ${rule.replacement}`} + )} + {rule.type === 'asterisk' && ( + {`Replace: ${rule.phrase} with ***`} + )} + + + + ))} + {redactionRules.length === 0 && No active redaction rules} + + +
+
+ + + + JS Function + +
+ onExpandDialogClicked(postProcessingFunction)} + > + + + + +
+ setPostProcessingFunction(code)} + basicSetup={{ highlightActiveLine: false, highlightActiveLineGutter: false }} + /> +
+
- - - The following variables are available to use in the custom function:{' '} -
$flow.rawOutput, $flow.input, $flow.chatflowId, $flow.sessionId, $flow.chatId
-
+
+ + + The following variables are available to use in the custom function:{' '} +
$flow.rawOutput, $flow.input, $flow.chatflowId, $flow.sessionId, $flow.chatId
+
+
-
+