Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,19 @@ ENV NODE_OPTIONS=--max-old-space-size=8192

WORKDIR /usr/src/flowise

# Copy app source
COPY . .
# Create workdir with correct ownership before copying files
RUN chown node:node /usr/src/flowise

# Switch to non-root user before copying and building
USER node

# Copy app source with correct ownership
COPY --chown=node:node . .

# Install dependencies and build (excluding sdk packages not needed for Docker)
RUN pnpm install && \
pnpm build:docker

# Give the node user ownership of the application files
RUN chown -R node:node .

# Switch to non-root user (node user already exists in node:20-alpine)
USER node

EXPOSE 3000

CMD [ "pnpm", "start" ]
CMD [ "pnpm", "start" ]
16 changes: 12 additions & 4 deletions packages/components/src/followUpPrompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,19 @@ export interface FollowUpPromptResult {
export const generateFollowUpPrompts = async (
followUpPromptsConfig: FollowUpPromptConfig,
apiMessageContent: string,
options: ICommonObject
options: ICommonObject,
sessionHistory?: string
): Promise<FollowUpPromptResult | undefined> => {
if (followUpPromptsConfig) {
if (!followUpPromptsConfig.status) return undefined
const providerConfig = followUpPromptsConfig[followUpPromptsConfig.selectedProvider]
if (!providerConfig) return undefined
const credentialId = providerConfig.credentialId as string
const credentialData = await getCredentialData(credentialId ?? '', options)
const followUpPromptsPrompt = providerConfig.prompt.replace('{history}', apiMessageContent)
let followUpPromptsPrompt = providerConfig.prompt.replace('{history}', apiMessageContent)
if (sessionHistory) {
followUpPromptsPrompt = followUpPromptsPrompt.replace('{session_history}', sessionHistory)
}
Comment on lines +35 to +38
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The use of .replace() only substitutes the first occurrence of the placeholder. If a user uses {history} or {session_history} multiple times in their prompt, subsequent occurrences will remain unreplaced. Additionally, using a string as the second argument to .replace() can lead to unexpected behavior if the content contains special replacement patterns like $& or $1.

It is recommended to use .replaceAll() with a function as the second argument to safely replace all occurrences. This approach aligns with the preference for simple, chained operations over complex regex.

        let followUpPromptsPrompt = providerConfig.prompt
            .replaceAll('{history}', () => apiMessageContent)
            .replaceAll('{session_history}', () => sessionHistory || '')
References
  1. Prioritize code readability and understandability over conciseness. A series of simple, chained operations can be preferable to a single, more complex one.


switch (followUpPromptsConfig.selectedProvider) {
case FollowUpPromptProvider.ANTHROPIC: {
Expand Down Expand Up @@ -70,10 +74,14 @@ export const generateFollowUpPrompts = async (
{format_instructions}
`)
const chain = prompt.pipe(llm).pipe(parser)
const structuredResponse = await chain.invoke({
const invokeParams: ICommonObject = {
history: apiMessageContent,
format_instructions: formatInstructions
})
}
if (sessionHistory) {
invokeParams.session_history = sessionHistory
}
const structuredResponse = await chain.invoke(invokeParams)
return structuredResponse as FollowUpPromptResult
}
case FollowUpPromptProvider.GOOGLE_GENAI: {
Expand Down
50 changes: 38 additions & 12 deletions packages/server/src/utils/buildChatflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -662,12 +662,25 @@ export const executeFlow = async ({

if (agentflow.followUpPrompts) {
const followUpPromptsConfig = JSON.parse(agentflow.followUpPrompts)
const generatedFollowUpPrompts = await generateFollowUpPrompts(followUpPromptsConfig, apiMessage.content, {
chatId,
chatflowid: agentflow.id,
appDataSource,
databaseEntities
})
// Format session history for the prompt
const formattedSessionHistory = chatHistory
.map((msg) => {
const role = msg.type === 'apiMessage' ? 'Assistant' : 'User'
const content = msg.message || msg.content || ''
return `${role}: ${content}`
})
.join('\n')
Comment on lines +666 to +672
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for formatting session history is duplicated here and at lines 892-898. To improve maintainability and ensure consistency, consider extracting this mapping logic into a shared utility function. Additionally, you might want to .trim() the content to avoid extra whitespace in the prompt.

const generatedFollowUpPrompts = await generateFollowUpPrompts(
followUpPromptsConfig,
apiMessage.content,
{
chatId,
chatflowid: agentflow.id,
appDataSource,
databaseEntities
},
formattedSessionHistory
)
if (generatedFollowUpPrompts?.questions) {
apiMessage.followUpPrompts = JSON.stringify(generatedFollowUpPrompts.questions)
}
Expand Down Expand Up @@ -875,12 +888,25 @@ export const executeFlow = async ({
if (result?.action) apiMessage.action = typeof result.action === 'string' ? result.action : JSON.stringify(result.action)
if (chatflow.followUpPrompts) {
const followUpPromptsConfig = JSON.parse(chatflow.followUpPrompts)
const followUpPrompts = await generateFollowUpPrompts(followUpPromptsConfig, apiMessage.content, {
chatId,
chatflowid,
appDataSource,
databaseEntities
})
// Format session history for the prompt
const formattedSessionHistory = chatHistory
.map((msg) => {
const role = msg.type === 'apiMessage' ? 'Assistant' : 'User'
const content = msg.message || msg.content || ''
return `${role}: ${content}`
})
.join('\n')
const followUpPrompts = await generateFollowUpPrompts(
followUpPromptsConfig,
apiMessage.content,
{
chatId,
chatflowid,
appDataSource,
databaseEntities
},
formattedSessionHistory
)
if (followUpPrompts?.questions) {
apiMessage.followUpPrompts = JSON.stringify(followUpPrompts.questions)
}
Expand Down
4 changes: 3 additions & 1 deletion packages/server/src/utils/getChatMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ export const utilGetChatMessage = async ({
},
order: {
createdDate: sortOrder === 'DESC' ? 'DESC' : 'ASC'
}
},
skip: page > -1 && pageSize > -1 ? page * pageSize : undefined,
take: page > -1 && pageSize > -1 ? pageSize : undefined
Comment on lines +119 to +120
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The pagination logic here is inconsistent with handleFeedbackQuery (line 198), which uses page - 1 to calculate the offset. This indicates that page is intended to be 1-indexed. Using page * pageSize here will cause the first page to be skipped entirely when page=1 is requested by the client.

Suggested change
skip: page > -1 && pageSize > -1 ? page * pageSize : undefined,
take: page > -1 && pageSize > -1 ? pageSize : undefined
skip: page > 0 && pageSize > -1 ? (page - 1) * pageSize : undefined,
take: page > 0 && pageSize > -1 ? pageSize : undefined

})

return messages
Expand Down
41 changes: 40 additions & 1 deletion packages/ui/src/views/tools/ToolDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set
const [toolName, setToolName] = useState('')
const [toolDesc, setToolDesc] = useState('')
const [toolIcon, setToolIcon] = useState('')
const [toolIconError, setToolIconError] = useState('')
const [toolSchema, setToolSchema] = useState([])
const [toolFunc, setToolFunc] = useState('')
const [showHowToDialog, setShowHowToDialog] = useState(false)
Expand All @@ -97,6 +98,31 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set
[]
)

const validateToolIconUrl = (url) => {
if (!url || url.trim() === '') {
setToolIconError('')
return true
}
try {
const urlObj = new URL(url)
if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') {
setToolIconError('Tool Icon Source must be a valid http or https URL')
return false
}
setToolIconError('')
return true
} catch (error) {
setToolIconError('Tool Icon Source must be a valid URL')
return false
}
}

const handleToolIconChange = (e) => {
const value = e.target.value
setToolIcon(value)
validateToolIconUrl(value)
}

const addNewRow = () => {
setTimeout(() => {
setToolSchema((prevRows) => {
Expand Down Expand Up @@ -225,6 +251,7 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set
setToolName('')
setToolDesc('')
setToolIcon('')
setToolIconError('')
setToolSchema([])
setToolFunc('')
}
Expand Down Expand Up @@ -277,6 +304,9 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set
}

const addNewTool = async () => {
if (!validateToolIconUrl(toolIcon)) {
return
}
try {
const obj = {
name: toolName,
Expand Down Expand Up @@ -323,6 +353,9 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set
}

const saveTool = async () => {
if (!validateToolIconUrl(toolIcon)) {
return
}
try {
const saveResp = await toolsApi.updateTool(toolId, {
name: toolName,
Expand Down Expand Up @@ -513,8 +546,14 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set
placeholder='https://raw.githubusercontent.com/gilbarbara/logos/main/logos/airtable.svg'
value={toolIcon}
name='toolIcon'
onChange={(e) => setToolIcon(e.target.value)}
onChange={handleToolIconChange}
error={!!toolIconError}
/>
{toolIconError && (
<Typography variant='caption' color='error' sx={{ mt: 0.5 }}>
{toolIconError}
</Typography>
)}
</Box>
<Box>
<Stack sx={{ position: 'relative', justifyContent: 'space-between' }} direction='row'>
Expand Down