Skip to content

Commit 8756019

Browse files
vinodkiranHenryHengZJ
authored andcommitted
New Feature: Add post postprocessing of response from LLM using custom JS Function (FlowiseAI#4079)
* New Feature: Add post postprocessing of response from LLM using custom Javascript functions * Disable Save when there is no content * add post processing ui changes, disable streaming --------- Co-authored-by: Henry <[email protected]>
1 parent b46405a commit 8756019

File tree

6 files changed

+297
-7
lines changed

6 files changed

+297
-7
lines changed

packages/components/nodes/utilities/CustomFunction/CustomFunction.ts

+1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ class CustomFunction_Utilities implements INode {
8888
chatflowId: options.chatflowid,
8989
sessionId: options.sessionId,
9090
chatId: options.chatId,
91+
rawOutput: options.rawOutput || '',
9192
input
9293
}
9394

packages/server/src/services/chatflows/index.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { removeFolderFromStorage } from 'flowise-components'
1+
import { ICommonObject, removeFolderFromStorage } from 'flowise-components'
22
import { StatusCodes } from 'http-status-codes'
33
import { ChatflowType, IReactFlowObject } from '../../Interface'
44
import { ChatFlow } from '../../database/entities/ChatFlow'
@@ -28,6 +28,15 @@ const checkIfChatflowIsValidForStreaming = async (chatflowId: string): Promise<a
2828
throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Chatflow ${chatflowId} not found`)
2929
}
3030

31+
/* Check for post-processing settings, if available isStreamValid is always false */
32+
let chatflowConfig: ICommonObject = {}
33+
if (chatflow.chatbotConfig) {
34+
chatflowConfig = JSON.parse(chatflow.chatbotConfig)
35+
if (chatflowConfig?.postProcessing?.enabled === true) {
36+
return { isStreaming: false }
37+
}
38+
}
39+
3140
/*** Get Ending Node with Directed Graph ***/
3241
const flowData = chatflow.flowData
3342
const parsedFlowData: IReactFlowObject = JSON.parse(flowData)

packages/server/src/utils/buildChatflow.ts

+44-3
Original file line numberDiff line numberDiff line change
@@ -579,7 +579,19 @@ export const executeFlow = async ({
579579
}
580580
return undefined
581581
} else {
582-
const isStreamValid = await checkIfStreamValid(endingNodes, nodes, streaming)
582+
let chatflowConfig: ICommonObject = {}
583+
if (chatflow.chatbotConfig) {
584+
chatflowConfig = JSON.parse(chatflow.chatbotConfig)
585+
}
586+
587+
let isStreamValid = false
588+
589+
/* Check for post-processing settings, if available isStreamValid is always false */
590+
if (chatflowConfig?.postProcessing?.enabled === true) {
591+
isStreamValid = false
592+
} else {
593+
isStreamValid = await checkIfStreamValid(endingNodes, nodes, streaming)
594+
}
583595

584596
/*** Find the last node to execute ***/
585597
const { endingNodeData, endingNodeInstance } = await initEndingNode({
@@ -637,8 +649,37 @@ export const executeFlow = async ({
637649
await utilAddChatMessage(userMessage, appDataSource)
638650

639651
let resultText = ''
640-
if (result.text) resultText = result.text
641-
else if (result.json) resultText = '```json\n' + JSON.stringify(result.json, null, 2)
652+
if (result.text) {
653+
resultText = result.text
654+
/* Check for post-processing settings */
655+
if (chatflowConfig?.postProcessing?.enabled === true) {
656+
try {
657+
const postProcessingFunction = JSON.parse(chatflowConfig?.postProcessing?.customFunction)
658+
const nodeInstanceFilePath = componentNodes['customFunction'].filePath as string
659+
const nodeModule = await import(nodeInstanceFilePath)
660+
const nodeData = {
661+
inputs: { javascriptFunction: postProcessingFunction },
662+
outputs: { output: 'output' }
663+
}
664+
const options: ICommonObject = {
665+
chatflowid: chatflow.id,
666+
sessionId,
667+
chatId,
668+
input: question,
669+
rawOutput: resultText,
670+
appDataSource,
671+
databaseEntities,
672+
logger
673+
}
674+
const customFuncNodeInstance = new nodeModule.nodeClass()
675+
let moderatedResponse = await customFuncNodeInstance.init(nodeData, question, options)
676+
result.text = moderatedResponse
677+
resultText = result.text
678+
} catch (e) {
679+
logger.log('[server]: Post Processing Error:', e)
680+
}
681+
}
682+
} else if (result.json) resultText = '```json\n' + JSON.stringify(result.json, null, 2)
642683
else resultText = JSON.stringify(result, null, 2)
643684

644685
const apiMessage: Omit<IChatMessage, 'createdDate'> = {

packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx

+13-3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import StarterPrompts from '@/ui-component/extended/StarterPrompts'
1111
import Leads from '@/ui-component/extended/Leads'
1212
import FollowUpPrompts from '@/ui-component/extended/FollowUpPrompts'
1313
import FileUpload from '@/ui-component/extended/FileUpload'
14+
import PostProcessing from '@/ui-component/extended/PostProcessing'
1415

1516
const CHATFLOW_CONFIGURATION_TABS = [
1617
{
@@ -44,6 +45,11 @@ const CHATFLOW_CONFIGURATION_TABS = [
4445
{
4546
label: 'File Upload',
4647
id: 'fileUpload'
48+
},
49+
{
50+
label: 'Post Processing',
51+
id: 'postProcessing',
52+
hideInAgentFlow: true
4753
}
4854
]
4955

@@ -76,10 +82,12 @@ function a11yProps(index) {
7682
}
7783
}
7884

79-
const ChatflowConfigurationDialog = ({ show, dialogProps, onCancel }) => {
85+
const ChatflowConfigurationDialog = ({ show, isAgentCanvas, dialogProps, onCancel }) => {
8086
const portalElement = document.getElementById('portal')
8187
const [tabValue, setTabValue] = useState(0)
8288

89+
const filteredTabs = CHATFLOW_CONFIGURATION_TABS.filter((tab) => !isAgentCanvas || !tab.hideInAgentFlow)
90+
8391
const component = show ? (
8492
<Dialog
8593
onClose={onCancel}
@@ -108,7 +116,7 @@ const ChatflowConfigurationDialog = ({ show, dialogProps, onCancel }) => {
108116
variant='scrollable'
109117
scrollButtons='auto'
110118
>
111-
{CHATFLOW_CONFIGURATION_TABS.map((item, index) => (
119+
{filteredTabs.map((item, index) => (
112120
<Tab
113121
sx={{
114122
minHeight: '40px',
@@ -123,7 +131,7 @@ const ChatflowConfigurationDialog = ({ show, dialogProps, onCancel }) => {
123131
></Tab>
124132
))}
125133
</Tabs>
126-
{CHATFLOW_CONFIGURATION_TABS.map((item, index) => (
134+
{filteredTabs.map((item, index) => (
127135
<TabPanel key={index} value={tabValue} index={index}>
128136
{item.id === 'security' && <Security dialogProps={dialogProps} />}
129137
{item.id === 'conversationStarters' ? <StarterPrompts dialogProps={dialogProps} /> : null}
@@ -133,6 +141,7 @@ const ChatflowConfigurationDialog = ({ show, dialogProps, onCancel }) => {
133141
{item.id === 'analyseChatflow' ? <AnalyseFlow dialogProps={dialogProps} /> : null}
134142
{item.id === 'leads' ? <Leads dialogProps={dialogProps} /> : null}
135143
{item.id === 'fileUpload' ? <FileUpload dialogProps={dialogProps} /> : null}
144+
{item.id === 'postProcessing' ? <PostProcessing dialogProps={dialogProps} /> : null}
136145
</TabPanel>
137146
))}
138147
</DialogContent>
@@ -144,6 +153,7 @@ const ChatflowConfigurationDialog = ({ show, dialogProps, onCancel }) => {
144153

145154
ChatflowConfigurationDialog.propTypes = {
146155
show: PropTypes.bool,
156+
isAgentCanvas: PropTypes.bool,
147157
dialogProps: PropTypes.object,
148158
onCancel: PropTypes.func
149159
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import { useDispatch } from 'react-redux'
2+
import { useState, useEffect } from 'react'
3+
import PropTypes from 'prop-types'
4+
import { useSelector } from 'react-redux'
5+
6+
// material-ui
7+
import { IconButton, Button, Box, Typography } from '@mui/material'
8+
import { IconArrowsMaximize, IconBulb, IconX } from '@tabler/icons-react'
9+
import { useTheme } from '@mui/material/styles'
10+
11+
// Project import
12+
import { StyledButton } from '@/ui-component/button/StyledButton'
13+
import { SwitchInput } from '@/ui-component/switch/Switch'
14+
import { CodeEditor } from '@/ui-component/editor/CodeEditor'
15+
import ExpandTextDialog from '@/ui-component/dialog/ExpandTextDialog'
16+
17+
// store
18+
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from '@/store/actions'
19+
import useNotifier from '@/utils/useNotifier'
20+
21+
// API
22+
import chatflowsApi from '@/api/chatflows'
23+
24+
const sampleFunction = `return $flow.rawOutput + " This is a post processed response!";`
25+
26+
const PostProcessing = ({ dialogProps }) => {
27+
const dispatch = useDispatch()
28+
29+
useNotifier()
30+
const theme = useTheme()
31+
const customization = useSelector((state) => state.customization)
32+
33+
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
34+
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
35+
36+
const [postProcessingEnabled, setPostProcessingEnabled] = useState(false)
37+
const [postProcessingFunction, setPostProcessingFunction] = useState('')
38+
const [chatbotConfig, setChatbotConfig] = useState({})
39+
const [showExpandDialog, setShowExpandDialog] = useState(false)
40+
const [expandDialogProps, setExpandDialogProps] = useState({})
41+
42+
const handleChange = (value) => {
43+
setPostProcessingEnabled(value)
44+
}
45+
46+
const onExpandDialogClicked = (value) => {
47+
const dialogProps = {
48+
value,
49+
inputParam: {
50+
label: 'Post Processing Function',
51+
name: 'postProcessingFunction',
52+
type: 'code',
53+
placeholder: sampleFunction,
54+
hideCodeExecute: true
55+
},
56+
languageType: 'js',
57+
confirmButtonName: 'Save',
58+
cancelButtonName: 'Cancel'
59+
}
60+
setExpandDialogProps(dialogProps)
61+
setShowExpandDialog(true)
62+
}
63+
64+
const onSave = async () => {
65+
try {
66+
let value = {
67+
postProcessing: {
68+
enabled: postProcessingEnabled,
69+
customFunction: JSON.stringify(postProcessingFunction)
70+
}
71+
}
72+
chatbotConfig.postProcessing = value.postProcessing
73+
const saveResp = await chatflowsApi.updateChatflow(dialogProps.chatflow.id, {
74+
chatbotConfig: JSON.stringify(chatbotConfig)
75+
})
76+
if (saveResp.data) {
77+
enqueueSnackbar({
78+
message: 'Post Processing Settings Saved',
79+
options: {
80+
key: new Date().getTime() + Math.random(),
81+
variant: 'success',
82+
action: (key) => (
83+
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
84+
<IconX />
85+
</Button>
86+
)
87+
}
88+
})
89+
dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data })
90+
}
91+
} catch (error) {
92+
enqueueSnackbar({
93+
message: `Failed to save Post Processing Settings: ${
94+
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
95+
}`,
96+
options: {
97+
key: new Date().getTime() + Math.random(),
98+
variant: 'error',
99+
persist: true,
100+
action: (key) => (
101+
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
102+
<IconX />
103+
</Button>
104+
)
105+
}
106+
})
107+
}
108+
}
109+
110+
useEffect(() => {
111+
if (dialogProps.chatflow && dialogProps.chatflow.chatbotConfig) {
112+
let chatbotConfig = JSON.parse(dialogProps.chatflow.chatbotConfig)
113+
setChatbotConfig(chatbotConfig || {})
114+
if (chatbotConfig.postProcessing) {
115+
setPostProcessingEnabled(chatbotConfig.postProcessing.enabled)
116+
if (chatbotConfig.postProcessing.customFunction) {
117+
setPostProcessingFunction(JSON.parse(chatbotConfig.postProcessing.customFunction))
118+
}
119+
}
120+
}
121+
122+
return () => {}
123+
}, [dialogProps])
124+
125+
return (
126+
<>
127+
<Box sx={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
128+
<SwitchInput label='Enable Post Processing' onChange={handleChange} value={postProcessingEnabled} />
129+
</Box>
130+
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 1 }}>
131+
<Box sx={{ width: '100%', display: 'flex', alignItems: 'center' }}>
132+
<Typography>JS Function</Typography>
133+
<Button
134+
sx={{ ml: 2 }}
135+
variant='outlined'
136+
onClick={() => {
137+
setPostProcessingFunction(sampleFunction)
138+
}}
139+
>
140+
See Example
141+
</Button>
142+
<div style={{ flex: 1 }} />
143+
<IconButton
144+
size='small'
145+
sx={{
146+
height: 25,
147+
width: 25
148+
}}
149+
title='Expand'
150+
color='primary'
151+
onClick={() => onExpandDialogClicked(postProcessingFunction)}
152+
>
153+
<IconArrowsMaximize />
154+
</IconButton>
155+
</Box>
156+
157+
<div
158+
style={{
159+
marginTop: '10px',
160+
border: '1px solid',
161+
borderColor: theme.palette.grey['300'],
162+
borderRadius: '6px',
163+
height: '200px',
164+
width: '100%'
165+
}}
166+
>
167+
<CodeEditor
168+
value={postProcessingFunction}
169+
height='200px'
170+
theme={customization.isDarkMode ? 'dark' : 'light'}
171+
lang={'js'}
172+
placeholder={sampleFunction}
173+
onValueChange={(code) => setPostProcessingFunction(code)}
174+
basicSetup={{ highlightActiveLine: false, highlightActiveLineGutter: false }}
175+
/>
176+
</div>
177+
</Box>
178+
<div
179+
style={{
180+
display: 'flex',
181+
flexDirection: 'column',
182+
borderRadius: 10,
183+
background: '#d8f3dc',
184+
padding: 10,
185+
marginTop: 10
186+
}}
187+
>
188+
<div
189+
style={{
190+
display: 'flex',
191+
flexDirection: 'row',
192+
alignItems: 'center',
193+
paddingTop: 10
194+
}}
195+
>
196+
<IconBulb size={30} color='#2d6a4f' />
197+
<span style={{ color: '#2d6a4f', marginLeft: 10, fontWeight: 500 }}>
198+
The following variables are available to use in the custom function:{' '}
199+
<pre>$flow.rawOutput, $flow.input, $flow.chatflowId, $flow.sessionId, $flow.chatId</pre>
200+
</span>
201+
</div>
202+
</div>
203+
<StyledButton
204+
style={{ marginBottom: 10, marginTop: 10 }}
205+
variant='contained'
206+
disabled={!postProcessingFunction || postProcessingFunction?.trim().length === 0}
207+
onClick={onSave}
208+
>
209+
Save
210+
</StyledButton>
211+
<ExpandTextDialog
212+
show={showExpandDialog}
213+
dialogProps={expandDialogProps}
214+
onCancel={() => setShowExpandDialog(false)}
215+
onConfirm={(newValue) => {
216+
setPostProcessingFunction(newValue)
217+
setShowExpandDialog(false)
218+
}}
219+
></ExpandTextDialog>
220+
</>
221+
)
222+
}
223+
224+
PostProcessing.propTypes = {
225+
dialogProps: PropTypes.object
226+
}
227+
228+
export default PostProcessing

packages/ui/src/views/canvas/CanvasHeader.jsx

+1
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,7 @@ const CanvasHeader = ({ chatflow, isAgentCanvas, handleSaveFlow, handleDeleteFlo
475475
show={chatflowConfigurationDialogOpen}
476476
dialogProps={chatflowConfigurationDialogProps}
477477
onCancel={() => setChatflowConfigurationDialogOpen(false)}
478+
isAgentCanvas={isAgentCanvas}
478479
/>
479480
</>
480481
)

0 commit comments

Comments
 (0)