changes & fixes for various issues in chatflow config (#4314)

* Chatflow config - rate limit and override config were overwriting changes.

* prevent post processing JS function from escaping as JSON...

* non-streaming follow up prompts need to be escaped twice

* prevent post processing JS function from escaping as JSON...

* Adding file mimetypes for full file upload...

* lint..

* fixing the issue with storing only filtered nodes..

* return doc store processing response without await when queue mode and request is from UI

---------

Co-authored-by: Henry <hzj94@hotmail.com>
This commit is contained in:
Vinod Kiran
2025-04-23 20:28:37 +05:30
committed by GitHub
parent 9c1652570e
commit 68d3c83980
9 changed files with 149 additions and 46 deletions
@@ -201,7 +201,8 @@ const processLoader = async (req: Request, res: Response, next: NextFunction) =>
} }
const docLoaderId = req.params.loaderId const docLoaderId = req.params.loaderId
const body = req.body const body = req.body
const apiResponse = await documentStoreService.processLoaderMiddleware(body, docLoaderId) const isInternalRequest = req.headers['x-request-from'] === 'internal'
const apiResponse = await documentStoreService.processLoaderMiddleware(body, docLoaderId, isInternalRequest)
return res.json(apiResponse) return res.json(apiResponse)
} catch (error) { } catch (error) {
next(error) next(error)
@@ -740,7 +740,7 @@ export const processLoader = async ({ appDataSource, componentNodes, data, docLo
return getDocumentStoreFileChunks(appDataSource, data.storeId as string, docLoaderId) return getDocumentStoreFileChunks(appDataSource, data.storeId as string, docLoaderId)
} }
const processLoaderMiddleware = async (data: IDocumentStoreLoaderForPreview, docLoaderId: string) => { const processLoaderMiddleware = async (data: IDocumentStoreLoaderForPreview, docLoaderId: string, isInternalRequest = false) => {
try { try {
const appServer = getRunningExpressApp() const appServer = getRunningExpressApp()
const appDataSource = appServer.AppDataSource const appDataSource = appServer.AppDataSource
@@ -761,6 +761,12 @@ const processLoaderMiddleware = async (data: IDocumentStoreLoaderForPreview, doc
const job = await upsertQueue.addJob(omit(executeData, OMIT_QUEUE_JOB_DATA)) const job = await upsertQueue.addJob(omit(executeData, OMIT_QUEUE_JOB_DATA))
logger.debug(`[server]: Job added to queue: ${job.id}`) logger.debug(`[server]: Job added to queue: ${job.id}`)
if (isInternalRequest) {
return {
jobId: job.id
}
}
const queueEvents = upsertQueue.getQueueEvents() const queueEvents = upsertQueue.getQueueEvents()
const result = await job.waitUntilFinished(queueEvents) const result = await job.waitUntilFinished(queueEvents)
+10 -2
View File
@@ -14,7 +14,8 @@ import {
mapMimeTypeToInputField, mapMimeTypeToInputField,
mapExtToInputField, mapExtToInputField,
getFileFromUpload, getFileFromUpload,
removeSpecificFileFromUpload removeSpecificFileFromUpload,
handleEscapeCharacters
} from 'flowise-components' } from 'flowise-components'
import { StatusCodes } from 'http-status-codes' import { StatusCodes } from 'http-status-codes'
import { import {
@@ -665,6 +666,7 @@ export const executeFlow = async ({
const postProcessingFunction = JSON.parse(chatflowConfig?.postProcessing?.customFunction) const postProcessingFunction = JSON.parse(chatflowConfig?.postProcessing?.customFunction)
const nodeInstanceFilePath = componentNodes['customFunction'].filePath as string const nodeInstanceFilePath = componentNodes['customFunction'].filePath as string
const nodeModule = await import(nodeInstanceFilePath) const nodeModule = await import(nodeInstanceFilePath)
//set the outputs.output to EndingNode to prevent json escaping of content...
const nodeData = { const nodeData = {
inputs: { javascriptFunction: postProcessingFunction }, inputs: { javascriptFunction: postProcessingFunction },
outputs: { output: 'output' } outputs: { output: 'output' }
@@ -681,7 +683,13 @@ export const executeFlow = async ({
} }
const customFuncNodeInstance = new nodeModule.nodeClass() const customFuncNodeInstance = new nodeModule.nodeClass()
let moderatedResponse = await customFuncNodeInstance.init(nodeData, question, options) let moderatedResponse = await customFuncNodeInstance.init(nodeData, question, options)
result.text = moderatedResponse if (typeof moderatedResponse === 'string') {
result.text = handleEscapeCharacters(moderatedResponse, true)
} else if (typeof moderatedResponse === 'object') {
result.text = '```json\n' + JSON.stringify(moderatedResponse, null, 2) + '\n```'
} else {
result.text = moderatedResponse
}
resultText = result.text resultText = result.text
} catch (e) { } catch (e) {
logger.log('[server]: Post Processing Error:', e) logger.log('[server]: Post Processing Error:', e)
@@ -12,25 +12,26 @@ const FollowUpPromptsCard = ({ isGrid, followUpPrompts, sx, onPromptClick }) =>
className={'button-container'} className={'button-container'}
sx={{ width: '100%', maxWidth: isGrid ? 'inherit' : '400px', p: 1.5, display: 'flex', gap: 1, ...sx }} sx={{ width: '100%', maxWidth: isGrid ? 'inherit' : '400px', p: 1.5, display: 'flex', gap: 1, ...sx }}
> >
{followUpPrompts.map((fp, index) => ( {Array.isArray(followUpPrompts) &&
<Chip followUpPrompts.map((fp, index) => (
label={fp} <Chip
className={'button'} label={fp}
key={index} className={'button'}
onClick={(e) => onPromptClick(fp, e)} key={index}
sx={{ onClick={(e) => onPromptClick(fp, e)}
backgroundColor: 'transparent', sx={{
border: '1px solid', backgroundColor: 'transparent',
boxShadow: '0px 2px 1px -1px rgba(0,0,0,0.2)', border: '1px solid',
color: '#2196f3', boxShadow: '0px 2px 1px -1px rgba(0,0,0,0.2)',
transition: 'all 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', color: '#2196f3',
'&:hover': { transition: 'all 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
backgroundColor: customization.isDarkMode ? 'rgba(0, 0, 0, 0.12)' : 'rgba(0, 0, 0, 0.05)', '&:hover': {
border: '1px solid' backgroundColor: customization.isDarkMode ? 'rgba(0, 0, 0, 0.12)' : 'rgba(0, 0, 0, 0.05)',
} border: '1px solid'
}} }
/> }}
))} />
))}
</Box> </Box>
) )
} }
@@ -5,7 +5,7 @@ import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackba
import parser from 'html-react-parser' import parser from 'html-react-parser'
// material-ui // material-ui
import { Button, Box } from '@mui/material' import { Button, Box, Typography } from '@mui/material'
import { IconX, IconBulb } from '@tabler/icons-react' import { IconX, IconBulb } from '@tabler/icons-react'
// Project import // Project import
@@ -22,6 +22,18 @@ const message = `Uploaded files will be parsed as strings and sent to the LLM. I
<br /> <br />
Refer <a href='https://docs.flowiseai.com/using-flowise/uploads#files' target='_blank'>docs</a> for more details.` Refer <a href='https://docs.flowiseai.com/using-flowise/uploads#files' target='_blank'>docs</a> for more details.`
const availableFileTypes = [
{ name: 'CSS', ext: 'text/css' },
{ name: 'CSV', ext: 'text/csv' },
{ name: 'HTML', ext: 'text/html' },
{ name: 'JSON', ext: 'application/json' },
{ name: 'Markdown', ext: 'text/markdown' },
{ name: 'PDF', ext: 'application/pdf' },
{ name: 'SQL', ext: 'application/sql' },
{ name: 'Text File', ext: 'text/plain' },
{ name: 'XML', ext: 'application/xml' }
]
const FileUpload = ({ dialogProps }) => { const FileUpload = ({ dialogProps }) => {
const dispatch = useDispatch() const dispatch = useDispatch()
@@ -31,16 +43,27 @@ const FileUpload = ({ dialogProps }) => {
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const [fullFileUpload, setFullFileUpload] = useState(false) const [fullFileUpload, setFullFileUpload] = useState(false)
const [allowedFileTypes, setAllowedFileTypes] = useState([])
const [chatbotConfig, setChatbotConfig] = useState({}) const [chatbotConfig, setChatbotConfig] = useState({})
const handleChange = (value) => { const handleChange = (value) => {
setFullFileUpload(value) setFullFileUpload(value)
} }
const handleAllowedFileTypesChange = (event) => {
const { checked, value } = event.target
if (checked) {
setAllowedFileTypes((prev) => [...prev, value])
} else {
setAllowedFileTypes((prev) => prev.filter((item) => item !== value))
}
}
const onSave = async () => { const onSave = async () => {
try { try {
const value = { const value = {
status: fullFileUpload status: fullFileUpload,
allowedUploadFileTypes: allowedFileTypes.join(',')
} }
chatbotConfig.fullFileUpload = value chatbotConfig.fullFileUpload = value
@@ -82,6 +105,9 @@ const FileUpload = ({ dialogProps }) => {
} }
useEffect(() => { useEffect(() => {
/* backward compatibility - by default, allow all */
const allowedFileTypes = availableFileTypes.map((fileType) => fileType.ext)
setAllowedFileTypes(allowedFileTypes)
if (dialogProps.chatflow) { if (dialogProps.chatflow) {
if (dialogProps.chatflow.chatbotConfig) { if (dialogProps.chatflow.chatbotConfig) {
try { try {
@@ -90,6 +116,10 @@ const FileUpload = ({ dialogProps }) => {
if (chatbotConfig.fullFileUpload) { if (chatbotConfig.fullFileUpload) {
setFullFileUpload(chatbotConfig.fullFileUpload.status) setFullFileUpload(chatbotConfig.fullFileUpload.status)
} }
if (chatbotConfig.fullFileUpload?.allowedUploadFileTypes) {
const allowedFileTypes = chatbotConfig.fullFileUpload.allowedUploadFileTypes.split(',')
setAllowedFileTypes(allowedFileTypes)
}
} catch (e) { } catch (e) {
setChatbotConfig({}) setChatbotConfig({})
} }
@@ -135,8 +165,44 @@ const FileUpload = ({ dialogProps }) => {
</div> </div>
<SwitchInput label='Enable Full File Upload' onChange={handleChange} value={fullFileUpload} /> <SwitchInput label='Enable Full File Upload' onChange={handleChange} value={fullFileUpload} />
</Box> </Box>
{/* TODO: Allow selection of allowed file types*/}
<StyledButton style={{ marginBottom: 10, marginTop: 10 }} variant='contained' onClick={onSave}> <Typography sx={{ fontSize: 14, fontWeight: 500, marginBottom: 1 }}>Allow Uploads of Type</Typography>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))',
gap: 15,
padding: 10,
width: '100%',
marginBottom: '10px'
}}
>
{availableFileTypes.map((fileType) => (
<div
key={fileType.ext}
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'start'
}}
>
<input
type='checkbox'
id={fileType.ext}
name={fileType.ext}
checked={allowedFileTypes.indexOf(fileType.ext) !== -1}
value={fileType.ext}
disabled={!fullFileUpload}
onChange={handleAllowedFileTypesChange}
/>
<label htmlFor={fileType.ext} style={{ marginLeft: 10 }}>
{fileType.name} ({fileType.ext})
</label>
</div>
))}
</div>
<StyledButton style={{ marginBottom: 10, marginTop: 20 }} variant='contained' onClick={onSave}>
Save Save
</StyledButton> </StyledButton>
</> </>
@@ -116,25 +116,27 @@ const OverrideConfig = ({ dialogProps }) => {
} }
const formatObj = () => { const formatObj = () => {
const obj = { let apiConfig = JSON.parse(dialogProps.chatflow.apiConfig)
overrideConfig: { status: overrideConfigStatus } if (apiConfig === null || apiConfig === undefined) {
apiConfig = {}
} }
let overrideConfig = { status: overrideConfigStatus }
if (overrideConfigStatus) { if (overrideConfigStatus) {
// loop through each key in nodeOverrides and filter out the enabled ones
const filteredNodeOverrides = {} const filteredNodeOverrides = {}
for (const key in nodeOverrides) { for (const key in nodeOverrides) {
filteredNodeOverrides[key] = nodeOverrides[key].filter((node) => node.enabled) filteredNodeOverrides[key] = nodeOverrides[key].filter((node) => node.enabled)
} }
obj.overrideConfig = { overrideConfig = {
...obj.overrideConfig, ...overrideConfig,
nodes: filteredNodeOverrides, nodes: filteredNodeOverrides,
variables: variableOverrides.filter((node) => node.enabled) variables: variableOverrides.filter((node) => node.enabled)
} }
} }
apiConfig.overrideConfig = overrideConfig
return obj return apiConfig
} }
const onNodeOverrideToggle = (node, property, status) => { const onNodeOverrideToggle = (node, property, status) => {
@@ -206,7 +208,7 @@ const OverrideConfig = ({ dialogProps }) => {
if (!overrideConfigStatus) { if (!overrideConfigStatus) {
setNodeOverrides(newNodeOverrides) setNodeOverrides(newNodeOverrides)
} else { } else {
const updatedNodeOverrides = { ...nodeOverrides } const updatedNodeOverrides = { ...newNodeOverrides }
Object.keys(updatedNodeOverrides).forEach((node) => { Object.keys(updatedNodeOverrides).forEach((node) => {
if (!seenNodes.has(node)) { if (!seenNodes.has(node)) {
@@ -19,7 +19,7 @@ import chatflowsApi from '@/api/chatflows'
// utils // utils
import useNotifier from '@/utils/useNotifier' import useNotifier from '@/utils/useNotifier'
const RateLimit = () => { const RateLimit = ({ dialogProps }) => {
const dispatch = useDispatch() const dispatch = useDispatch()
const chatflow = useSelector((state) => state.canvas.chatflow) const chatflow = useSelector((state) => state.canvas.chatflow)
const chatflowid = chatflow.id const chatflowid = chatflow.id
@@ -36,9 +36,11 @@ const RateLimit = () => {
const [limitMsg, setLimitMsg] = useState(apiConfig?.rateLimit?.limitMsg ?? '') const [limitMsg, setLimitMsg] = useState(apiConfig?.rateLimit?.limitMsg ?? '')
const formatObj = () => { const formatObj = () => {
const obj = { let apiConfig = JSON.parse(dialogProps.chatflow.apiConfig)
rateLimit: { status: rateLimitStatus } if (apiConfig === null || apiConfig === undefined) {
apiConfig = {}
} }
let obj = { status: rateLimitStatus }
if (rateLimitStatus) { if (rateLimitStatus) {
const rateLimitValuesBoolean = [!limitMax, !limitDuration, !limitMsg] const rateLimitValuesBoolean = [!limitMax, !limitDuration, !limitMsg]
@@ -46,16 +48,16 @@ const RateLimit = () => {
if (rateLimitFilledValues.length >= 1 && rateLimitFilledValues.length <= 2) { if (rateLimitFilledValues.length >= 1 && rateLimitFilledValues.length <= 2) {
throw new Error('Need to fill all rate limit input fields') throw new Error('Need to fill all rate limit input fields')
} else if (rateLimitFilledValues.length === 3) { } else if (rateLimitFilledValues.length === 3) {
obj.rateLimit = { obj = {
...obj.rateLimit, ...obj,
limitMax, limitMax,
limitDuration, limitDuration,
limitMsg limitMsg
} }
} }
} }
apiConfig.rateLimit = obj
return obj return apiConfig
} }
const handleChange = (value) => { const handleChange = (value) => {
@@ -173,7 +175,8 @@ const RateLimit = () => {
} }
RateLimit.propTypes = { RateLimit.propTypes = {
isSessionMemory: PropTypes.bool isSessionMemory: PropTypes.bool,
dialogProps: PropTypes.object
} }
export default RateLimit export default RateLimit
@@ -12,7 +12,7 @@ const Security = ({ dialogProps }) => {
return ( return (
<Stack direction='column' divider={<Divider sx={{ my: 0.5, borderColor: theme.palette.grey[900] + 25 }} />} spacing={4}> <Stack direction='column' divider={<Divider sx={{ my: 0.5, borderColor: theme.palette.grey[900] + 25 }} />} spacing={4}>
<RateLimit /> <RateLimit dialogProps={dialogProps} />
<AllowedDomains dialogProps={dialogProps} /> <AllowedDomains dialogProps={dialogProps} />
<OverrideConfig dialogProps={dialogProps} /> <OverrideConfig dialogProps={dialogProps} />
</Stack> </Stack>
@@ -200,6 +200,7 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
// full file upload // full file upload
const [fullFileUpload, setFullFileUpload] = useState(false) const [fullFileUpload, setFullFileUpload] = useState(false)
const [fullFileUploadAllowedTypes, setFullFileUploadAllowedTypes] = useState('*')
// feedback // feedback
const [chatFeedbackStatus, setChatFeedbackStatus] = useState(false) const [chatFeedbackStatus, setChatFeedbackStatus] = useState(false)
@@ -693,7 +694,11 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
if (data.followUpPrompts) { if (data.followUpPrompts) {
const followUpPrompts = JSON.parse(data.followUpPrompts) const followUpPrompts = JSON.parse(data.followUpPrompts)
setFollowUpPrompts(followUpPrompts) if (typeof followUpPrompts === 'string') {
setFollowUpPrompts(JSON.parse(followUpPrompts))
} else {
setFollowUpPrompts(followUpPrompts)
}
} }
} }
@@ -981,7 +986,9 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
} }
const getFileUploadAllowedTypes = () => { const getFileUploadAllowedTypes = () => {
if (fullFileUpload) return '*' if (fullFileUpload) {
return fullFileUploadAllowedTypes === '' ? '*' : fullFileUploadAllowedTypes
}
return fileUploadAllowedTypes.includes('*') ? '*' : fileUploadAllowedTypes || '*' return fileUploadAllowedTypes.includes('*') ? '*' : fileUploadAllowedTypes || '*'
} }
@@ -1118,6 +1125,9 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
if (config.fullFileUpload) { if (config.fullFileUpload) {
setFullFileUpload(config.fullFileUpload.status) setFullFileUpload(config.fullFileUpload.status)
if (config.fullFileUpload?.allowedUploadFileTypes) {
setFullFileUploadAllowedTypes(config.fullFileUpload?.allowedUploadFileTypes)
}
} }
} }
} }
@@ -1198,7 +1208,13 @@ export const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, preview
if (followUpPromptsStatus && messages.length > 0) { if (followUpPromptsStatus && messages.length > 0) {
const lastMessage = messages[messages.length - 1] const lastMessage = messages[messages.length - 1]
if (lastMessage.type === 'apiMessage' && lastMessage.followUpPrompts) { if (lastMessage.type === 'apiMessage' && lastMessage.followUpPrompts) {
setFollowUpPrompts(lastMessage.followUpPrompts) if (Array.isArray(lastMessage.followUpPrompts)) {
setFollowUpPrompts(lastMessage.followUpPrompts)
}
if (typeof lastMessage.followUpPrompts === 'string') {
const followUpPrompts = JSON.parse(lastMessage.followUpPrompts)
setFollowUpPrompts(followUpPrompts)
}
} else if (lastMessage.type === 'userMessage') { } else if (lastMessage.type === 'userMessage') {
setFollowUpPrompts([]) setFollowUpPrompts([])
} }